// MobileGL - MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.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_Texture.h"
#include "Config.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h"
#include "MG_Util/Types.h"
#include "Validators.h"
#include "ProxyTexture.h"

#include <MG_State/GLState/Core.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
#include <MG_Util/Classifiers/TextureEnumClassifier.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include <MG_Util/Converters/MGToStr/TextureEnumConverter.h>
#include <MG_Impl/GLImpl/Framebuffer/Validators.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Sampler/Validators.h>
#include <MG_Util/Math/FixedPointConversion.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>

namespace MobileGL::MG_Impl::GLImpl {
    static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
    static UnorderedMap<Uint, Bool> g_autoGenerateMipmapByTextureId;

    Bool GetTexParameteriv_State(GLenum target, GLenum pname, GLint* params);

    namespace {
        void SetTextureBorderColorFromFloats(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                             const GLfloat* params) {
            textureObject->SetBorderColor(FloatVec4(params[0], params[1], params[2], params[3]));
        }

        // glTexParameteriv(GL_TEXTURE_BORDER_COLOR): GL 4.6 core 8.10 sends the components through
        // equation 2.2 into the floating-point border colour. glGetTexParameteriv reverses it with
        // equation 2.3; the two live in one header so they cannot drift apart.
        void SetTextureBorderColorFromInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                           const GLint* params) {
            textureObject->SetBorderColor(FloatVec4(MG_Util::SignedNormalizedInt32ToFloat(params[0]),
                                                    MG_Util::SignedNormalizedInt32ToFloat(params[1]),
                                                    MG_Util::SignedNormalizedInt32ToFloat(params[2]),
                                                    MG_Util::SignedNormalizedInt32ToFloat(params[3])));
        }

        void SetTextureBorderColorFromIntegerInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                                  const GLint* params) {
            textureObject->SetBorderColorI(IntVec4(params[0], params[1], params[2], params[3]));
        }

        void SetTextureBorderColorFromUnsignedInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                                   const GLuint* params) {
            textureObject->SetBorderColorUI(UintVec4(params[0], params[1], params[2], params[3]));
        }

        Bool SetTextureSwizzleParamsFromInts(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                             const GLint* params, const char* caller) {
            Vec4<TextureSwizzleParam> swizzleParams;
            for (int i = 0; i < 4; ++i) {
                swizzleParams[i] = MG_Util::ConvertGLEnumToTextureSwizzleParam(params[i]);
                if (TextureSwizzleParam::Unknown == swizzleParams[i]) {
                    MG_State::pGLContext->RecordError(
                        ErrorCode::InvalidEnum,
                        MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "`params` is not valid."));
                    return false;
                }
            }
            textureObject->SetSwizzleParamRGBA(swizzleParams);
            return true;
        }

        Bool ValidateMaxAnisotropy(Float maxAnisotropy, const char* caller) {
            if (maxAnisotropy >= 1.0f) return true;

            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "GL_TEXTURE_MAX_ANISOTROPY_EXT must be at least 1.0."));
            return false;
        }

        // DSA emulation: the by-name entry points are implemented by putting the named texture
        // on the active unit's slot for their target, running the classic bound-texture code,
        // then putting the previous binding back.
        //
        // Both of those binds are REAL changes to "which texture is bound at this unit" for as
        // long as `fn` runs, so both have to move the texture bind generation. Backends memoise
        // per-unit work keyed on that generation and BORROW the binding slot (they hold a
        // pointer to the slot's shared_ptr, not a copy); a slot swap the generation never saw
        // let such a memo replay texture A's backend twin against texture B now sitting in the
        // slot - which re-specified A's backend storage with B's shape and silently destroyed
        // A's GPU-rendered contents (Minecraft's lightmap, blanked by a by-name upload to an
        // Iris shadow map, which then discarded every glyph).
        //
        // The generation is bumped directly rather than through NoteTextureUnitTouched because
        // the touched-unit HIGH-WATER MARK must NOT move: glActiveTexture does not advance it,
        // so a DSA-only app would otherwise have every later draw walk up to the highest unit it
        // ever aimed a by-name call at. Not advancing it is also sufficient - a unit above the
        // mark is outside every memo's coverage and outside the epoch walk, so nothing can
        // observe the transient swap there; at or below it, the bump is exactly what makes the
        // epoch re-derive. Bumping only on a real change keeps the very common redundant case (a
        // by-name call on the texture already bound to the active unit) free.
        //
        // The restore is a scope guard because `fn` can throw (the unsupported-state paths use
        // THROW_EXCEPTION): leaking the temporary binding would leave the wrong texture bound to
        // a live unit for the rest of the context's life.
        template <typename Fn>
        void WithTemporarilyBoundNamedTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                             Fn&& fn) {
            if (!textureObject) return;

            const Int activeUnitIndex = MG_State::pGLContext->GetActiveTextureUnit();
            auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnitIndex);
            auto& bindingSlot = activeUnit.GetBindingSlot(textureObject->GetTarget());
            const auto previousBinding = bindingSlot.GetBoundObject();

            using SlotType = std::remove_reference_t<decltype(bindingSlot)>;
            class ScopedSlotRestore {
            public:
                ScopedSlotRestore(SlotType& slot, SharedPtr<MG_State::GLState::ITextureObject> previous)
                    : m_slot(slot), m_previous(Move(previous)) {}
                ~ScopedSlotRestore() {
                    if (m_slot.Bind(m_previous)) {
                        MG_State::pGLContext->BumpTextureBindGeneration();
                    }
                }
                ScopedSlotRestore(const ScopedSlotRestore&) = delete;
                ScopedSlotRestore& operator=(const ScopedSlotRestore&) = delete;

            private:
                SlotType& m_slot;
                SharedPtr<MG_State::GLState::ITextureObject> m_previous;
            };

            if (bindingSlot.Bind(textureObject)) {
                MG_State::pGLContext->BumpTextureBindGeneration();
            }
            ScopedSlotRestore restore(bindingSlot, previousBinding);

            fn(MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()));
        }

        SizeT ComputeTextureStorageByteSize(TextureInternalFormat textureInternalFormat, GLsizei width, GLsizei height,
                                            GLsizei depth) {
            GLenum realInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
            GLenum realFormat = GL_RGBA;
            GLenum realType = GL_UNSIGNED_BYTE;
            MG_Util::TextureFormatProcessor::NormalizePixelFormat(
                realInternalFormat, PixelFormatNormalizeOptionBit::None, &realInternalFormat, &realFormat, &realType);
            return static_cast<SizeT>(width) * static_cast<SizeT>(height) * static_cast<SizeT>(depth) *
                   MG_Util::GetInternalBytesPerPixel(textureInternalFormat,
                                                     MG_Util::ConvertGLEnumToTexturePixelDataType(realType));
        }

        Bool IsSizedTextureStorageInternalFormat(TextureInternalFormat textureInternalFormat) {
            switch (textureInternalFormat) {
            case TextureInternalFormat::Red:
            case TextureInternalFormat::RG:
            case TextureInternalFormat::RGB:
            case TextureInternalFormat::RGBA:
            case TextureInternalFormat::DepthComponent:
            case TextureInternalFormat::DepthStencil:
            case TextureInternalFormat::Unknown:
                return false;
            default:
                return true;
            }
        }

        Bool ValidateTextureStorageInternalFormat(TextureInternalFormat textureInternalFormat, const char* caller) {
            // TODO: Replace this sized-format filter with the full ARB_texture_storage legal-format table.
            if (!IsSizedTextureStorageInternalFormat(textureInternalFormat)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "TexStorage requires a sized internal format."));
                return false;
            }
            return TextureImpl::ValidateTextureInternalFormat(textureInternalFormat);
        }

        // How many LAYERS a texture of this target has, given its base level's state-side extent.
        // GL keeps a 1D array's layer count in the height and every other layered target's in the
        // depth; a cube map has exactly six and a 3D texture has one (its depth is spatial).
        Uint LayerCountOfImmutableTexture(TextureTarget target, const IntVec3& baseSize) {
            switch (target) {
            case TextureTarget::Texture1DArray:
                return static_cast<Uint>(std::max(baseSize.y(), 1));
            case TextureTarget::Texture2DArray:
            case TextureTarget::TextureCubeMapArray:
            case TextureTarget::Texture2DMultisampleArray:
                return static_cast<Uint>(std::max(baseSize.z(), 1));
            case TextureTarget::TextureCubeMap:
                return 6;
            default:
                return 1;
            }
        }

        // GL 4.6 core 8.19: TexStorage* leaves the texture describing itself as a full-extent view
        // of its own storage - TEXTURE_VIEW_MIN_LEVEL 0, TEXTURE_VIEW_NUM_LEVELS <levels>,
        // TEXTURE_VIEW_MIN_LAYER 0, TEXTURE_VIEW_NUM_LAYERS the layer count. That is not just a
        // query detail: glTextureView COMPOSES onto these ("<numlevels> and the value of
        // TEXTURE_VIEW_NUM_LEVELS from the original texture minus <minlevel>", 8.18), so leaving
        // them at the mutable-texture default of 0 would clamp every view to zero levels.
        void SeedImmutableViewState(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, Uint levels) {
            if (!textureObject) return;
            textureObject->SetViewLevelLayerRange(
                0, levels, 0,
                LayerCountOfImmutableTexture(textureObject->GetTarget(), textureObject->GetBaseSize()));
        }

        Bool ValidateTextureMutable(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                    const char* caller) {
            if (!textureObject || !textureObject->IsImmutable()) return true;

            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Immutable texture storage cannot be redefined."));
            return false;
        }

        GLint GetTextureComponentType(TextureInternalFormat textureInternalFormat, GLint size, Bool depthComponent,
                                      Bool stencilComponent) {
            if (size <= 0) return GL_NONE;
            if (stencilComponent) return GL_UNSIGNED_INT;
            if (depthComponent) {
                return (textureInternalFormat == TextureInternalFormat::DepthComponent32F ||
                        textureInternalFormat == TextureInternalFormat::Depth32FStencil8)
                           ? GL_FLOAT
                           : GL_UNSIGNED_NORMALIZED;
            }
            switch (textureInternalFormat) {
            case TextureInternalFormat::R8I:
            case TextureInternalFormat::R16I:
            case TextureInternalFormat::R32I:
            case TextureInternalFormat::RG8I:
            case TextureInternalFormat::RG16I:
            case TextureInternalFormat::RG32I:
            case TextureInternalFormat::RGB8I:
            case TextureInternalFormat::RGB16I:
            case TextureInternalFormat::RGB32I:
            case TextureInternalFormat::RGBA8I:
            case TextureInternalFormat::RGBA16I:
            case TextureInternalFormat::RGBA32I:
                return GL_INT;
            case TextureInternalFormat::R8UI:
            case TextureInternalFormat::R16UI:
            case TextureInternalFormat::R32UI:
            case TextureInternalFormat::RG8UI:
            case TextureInternalFormat::RG16UI:
            case TextureInternalFormat::RG32UI:
            case TextureInternalFormat::RGB8UI:
            case TextureInternalFormat::RGB16UI:
            case TextureInternalFormat::RGB32UI:
            case TextureInternalFormat::RGBA8UI:
            case TextureInternalFormat::RGBA16UI:
            case TextureInternalFormat::RGBA32UI:
            case TextureInternalFormat::RGB10A2UI:
                return GL_UNSIGNED_INT;
            case TextureInternalFormat::R16F:
            case TextureInternalFormat::RG16F:
            case TextureInternalFormat::RGB16F:
            case TextureInternalFormat::RGBA16F:
            case TextureInternalFormat::R32F:
            case TextureInternalFormat::RG32F:
            case TextureInternalFormat::RGB32F:
            case TextureInternalFormat::RGBA32F:
            case TextureInternalFormat::R11FG11FB10F:
            case TextureInternalFormat::RGB9E5:
                return GL_FLOAT;
            case TextureInternalFormat::R8Snorm:
            case TextureInternalFormat::R16Snorm:
            case TextureInternalFormat::RG8Snorm:
            case TextureInternalFormat::RG16Snorm:
            case TextureInternalFormat::RGB8Snorm:
            case TextureInternalFormat::RGB16Snorm:
            case TextureInternalFormat::RGBA8Snorm:
            case TextureInternalFormat::RGBA16Snorm:
                return GL_SIGNED_NORMALIZED;
            default:
                return GL_UNSIGNED_NORMALIZED;
            }
        }

        // TextureInternalFormat has no compressed enumerator and CompressedTexImage* rejects every
        // compressed format up front, so no texture image MobileGL holds can be compressed. Written
        // as a predicate rather than a literal false so both level-parameter getters stay in step
        // once compressed formats do land.
        // GL 4.6 core 8.11 asks "is *this level* stored compressed", not "is the texture's internal
        // format a compressed one", and here the two genuinely differ: a compressed internalformat
        // handed to glTexImage2D resolves to the uncompressed storage that backs it (see
        // ConvertGLEnumToTextureInternalFormat), so the texture's format enum can never answer yes.
        // The only levels stored compressed are the ones glCompressedTexImage* shadowed verbatim,
        // which is exactly what the per-level compressed format records.
        //
        // No level-count guard on purpose: TextureObject2DCube::GetMipmapLevelCount() reports face
        // zero's chain only, so a count check would answer GL_NONE for a compressed image on any
        // other face - precisely the per-face independence the storage layer provides. MipmapStorage's
        // own getters already bounds-check per target and return GL_NONE for an unallocated level.
        GLenum GetCompressedLevelFormat(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                        TextureUploadTarget uploadTarget, GLint level) {
            if (!textureObject || level < 0) return GL_NONE;
            const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
            if (!textureMipmapObject) return GL_NONE;
            return textureMipmapObject->GetMipmapCompressedFormat(uploadTarget, static_cast<Uint>(level));
        }

        GLint GetTextureLevelComponentParameter(TextureInternalFormat textureInternalFormat, GLenum pname) {
            const ComponentSizes componentSizes = MG_Util::GetComponentSizesForInternalFormat(textureInternalFormat);
            switch (pname) {
            case GL_TEXTURE_RED_SIZE:
                return componentSizes.Red;
            case GL_TEXTURE_GREEN_SIZE:
                return componentSizes.Green;
            case GL_TEXTURE_BLUE_SIZE:
                return componentSizes.Blue;
            case GL_TEXTURE_ALPHA_SIZE:
                return componentSizes.Alpha;
            case GL_TEXTURE_DEPTH_SIZE:
                return componentSizes.Depth;
            case GL_TEXTURE_STENCIL_SIZE:
                return componentSizes.Stencil;
            case GL_TEXTURE_RED_TYPE:
                return GetTextureComponentType(textureInternalFormat, componentSizes.Red, false, false);
            case GL_TEXTURE_GREEN_TYPE:
                return GetTextureComponentType(textureInternalFormat, componentSizes.Green, false, false);
            case GL_TEXTURE_BLUE_TYPE:
                return GetTextureComponentType(textureInternalFormat, componentSizes.Blue, false, false);
            case GL_TEXTURE_ALPHA_TYPE:
                return GetTextureComponentType(textureInternalFormat, componentSizes.Alpha, false, false);
            case GL_TEXTURE_DEPTH_TYPE:
                return GetTextureComponentType(textureInternalFormat, componentSizes.Depth, true, false);
            case GL_TEXTURE_SHARED_SIZE:
                // GL 4.6 core table 8.24: the size in bits of the SHARED EXPONENT, which only the
                // one shared-exponent format has. Everything else answers zero, and the
                // conformance suite compares "at least", not "equal".
                return textureInternalFormat == TextureInternalFormat::RGB9E5 ? 5 : 0;
            default:
                MOBILEGL_ASSERT(false, "Invalid texture level component pname: %d", pname);
                return 0;
            }
        }

        Bool IsValidImageTextureFormat(GLenum format) {
            switch (format) {
            case GL_RGBA32F:
            case GL_RGBA16F:
            case GL_RG32F:
            case GL_RG16F:
            case GL_R11F_G11F_B10F:
            case GL_R32F:
            case GL_R16F:
            case GL_RGBA32UI:
            case GL_RGBA16UI:
            case GL_RGB10_A2UI:
            case GL_RGBA8UI:
            case GL_RG32UI:
            case GL_RG16UI:
            case GL_RG8UI:
            case GL_R32UI:
            case GL_R16UI:
            case GL_R8UI:
            case GL_RGBA32I:
            case GL_RGBA16I:
            case GL_RGBA8I:
            case GL_RG32I:
            case GL_RG16I:
            case GL_RG8I:
            case GL_R32I:
            case GL_R16I:
            case GL_R8I:
            case GL_RGBA16:
            case GL_RGB10_A2:
            case GL_RGBA8:
            case GL_RG16:
            case GL_RG8:
            case GL_R16:
            case GL_R8:
            case GL_RGBA16_SNORM:
            case GL_RGBA8_SNORM:
            case GL_RG16_SNORM:
            case GL_RG8_SNORM:
            case GL_R16_SNORM:
            case GL_R8_SNORM:
                return true;
            default:
                return false;
            }
        }

        GLuint GetAdvertisedImageUnitCount() {
            return static_cast<GLuint>(std::min<GLint>(
                MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxImageUnits,
                MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS));
        }

        // How many components of a GL-space texel size actually halve down the mip chain.
        //
        // An array texture's LAYER COUNT is not a dimension of the image (GL 4.6 core 8.14.3): it
        // stays put all the way down, and it is stored in whichever component sits after the
        // image's own dimensions - z for a 2D array or a cube array, and HEIGHT for a 1D array,
        // whose level is recorded as {width, layers, 1}.
        //
        // THE one statement of that rule on the frontend side, because three readers have to agree
        // on it or a chain is allocated under one and judged under another: this allocator,
        // ComputeMipmapCompleteForFilter (MG_State/GLState/TextureState/TextureObject.cpp, which
        // uses the identical 1/2/3 split) and DirectVulkan's MipShrinkingComponentCount. It used to
        // be a two-way `depthMips` flag, which had no way to say "height is not a dimension" - so
        // glGenerateMipmap on a GL_TEXTURE_1D_ARRAY allocated a chain whose LAYER COUNT halved,
        // and the completeness rule then rejected the texture the generate was supposed to make
        // complete. The backend allocator could not repair it either: it only ever GROWS a chain,
        // and the frontend's (wrong) count is always the longer of the two.
        Int MipShrinkingAxisCount(TextureTarget target) {
            switch (target) {
            case TextureTarget::Texture1D:
                // {width, 1, 1} - the other two are already 1, but say so rather than rely on it.
                return 1;
            case TextureTarget::Texture1DArray:
                // {width, layers, 1}: height IS the layer count.
                return 1;
            case TextureTarget::Texture2DArray:
            case TextureTarget::TextureCubeMapArray:
                // {width, height, layers}: depth IS the layer count.
                return 2;
            case TextureTarget::Texture3D:
                return 3;
            default:
                // 2D, cube faces, rectangle, multisample: a plain two-dimensional image.
                return 2;
            }
        }

        // Only true 3D textures halve their depth per level; every array target keeps its layer
        // count. Expressed through the rule above so the two cannot drift.
        Bool DepthParticipatesInMipmapping(TextureTarget target) {
            return MipShrinkingAxisCount(target) == 3;
        }

        // Which targets each glTextureStorage*D accepts (GL 4.6 core 8.19). A texture whose target
        // belongs to a different one of the three is the wrong object, not a bad argument, so it is
        // INVALID_OPERATION.
        Bool IsTextureStorageTargetForDimension(TextureTarget target, int dimension) {
            switch (dimension) {
            case 1:
                return target == TextureTarget::Texture1D;
            case 2:
                return target == TextureTarget::Texture2D || target == TextureTarget::Texture1DArray ||
                       target == TextureTarget::TextureRectangle || target == TextureTarget::TextureCubeMap;
            case 3:
                return target == TextureTarget::Texture3D || target == TextureTarget::Texture2DArray ||
                       target == TextureTarget::TextureCubeMapArray;
            default:
                return false;
            }
        }

        // The longest mip chain the level-0 size admits, over the axes that actually reduce.
        Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes);

        Uint MaxTextureStorageLevels(TextureTarget target, GLsizei width, GLsizei height, GLsizei depth) {
            return ComputeFullMipmapLevelCount(
                {std::max<Int>(width, 1), std::max<Int>(height, 1), std::max<Int>(depth, 1)},
                MipShrinkingAxisCount(target));
        }

        Uint ComputeFullMipmapLevelCount(const IntVec3& baseTexelSize, Int shrinkingAxes) {
            Int maxDimension = 1;
            for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
                maxDimension = std::max<Int>(maxDimension, baseTexelSize[axis]);
            }
            Uint mipLevelCount = 1;
            while (maxDimension > 1) {
                maxDimension = std::max<Int>(maxDimension / 2, 1);
                ++mipLevelCount;
            }
            return mipLevelCount;
        }

        IntVec3 ComputeMipmapTexelSize(const IntVec3& baseTexelSize, Uint relativeLevel, Int shrinkingAxes) {
            IntVec3 size = {std::max<Int>(baseTexelSize.x(), 1), std::max<Int>(baseTexelSize.y(), 1),
                            std::max<Int>(baseTexelSize.z(), 1)};
            for (Int axis = 0; axis < shrinkingAxes && axis < 3; ++axis) {
                size[axis] = std::max<Int>(size[axis] >> static_cast<Int>(relativeLevel), 1);
            }
            return size;
        }

        Bool EnsureGeneratedMipmapStorageAllocated(
            MG_State::GLState::TextureObjectMipmap& texture,
            TextureUploadTarget uploadTarget) {
            const Uint existingLevelCount = texture.GetMipmapLevelCount();
            if (existingLevelCount == 0) {
                return false;
            }

            const IntVec3 baseTexelSize = texture.GetMipmapTexelSize(uploadTarget, 0);
            const SizeT baseByteSize = texture.GetMipmapByteSize(uploadTarget, 0);
            const SizeT baseTexelCount = static_cast<SizeT>(baseTexelSize.x()) *
                                         static_cast<SizeT>(baseTexelSize.y()) *
                                         static_cast<SizeT>(baseTexelSize.z());
            if (baseTexelSize.x() <= 0 || baseTexelSize.y() <= 0 || baseTexelSize.z() <= 0 ||
                baseByteSize == 0 || baseTexelCount == 0 || (baseByteSize % baseTexelCount) != 0) {
                return false;
            }

            const SizeT bytesPerTexel = baseByteSize / baseTexelCount;
            const Int shrinkingAxes = MipShrinkingAxisCount(texture.GetTarget());
            const Uint requiredLevelCount = ComputeFullMipmapLevelCount(baseTexelSize, shrinkingAxes);
            for (Uint level = 1; level < requiredLevelCount; ++level) {
                const IntVec3 levelTexelSize = ComputeMipmapTexelSize(baseTexelSize, level, shrinkingAxes);
                const SizeT levelByteSize = bytesPerTexel * static_cast<SizeT>(levelTexelSize.x()) *
                                            static_cast<SizeT>(levelTexelSize.y()) *
                                            static_cast<SizeT>(levelTexelSize.z());
                texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
                texture.MarkStorageDirty(uploadTarget, level, false);
            }
            // glGenerateMipmap defines exactly levels 0..requiredLevelCount-1. AllocateStorage only
            // grows, so a previously longer chain (a bigger base image before respecification) would
            // otherwise keep a tail of stale levels here and read as incomplete.
            texture.TruncateMipmapLevels(uploadTarget, requiredLevelCount);
            // Mip generation grows/regenerates the level set on the GPU without marking any CPU
            // level dirty (MarkStorageDirty(...,false) above). Bump the content version so the
            // backend re-syncs: a cached sampled VkImageView built for the pre-generate level
            // range would otherwise stay stale and clamp LOD>0 sampling to mip 0.
            texture.BumpContentVersion();
            return true;
        }

        void EnsureGeneratedMipmapStorageAllocated(MG_State::GLState::TextureObjectMipmap& texture) {
            for (const TextureUploadTarget uploadTarget : texture.GetUploadTargets()) {
                EnsureGeneratedMipmapStorageAllocated(texture, uploadTarget);
            }
        }

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

        // The largest count the backend actually probed for this format on this target, or 0 when
        // it has no answer for the pair. Both backends build the list in descending order.
        Int GetProbedMaxTextureSamples(TextureTarget textureTarget, TextureInternalFormat textureInternalFormat) {
            if (MG_Backend::pActiveBackendObject == nullptr) {
                return 0;
            }
            const SizeT targetIndex = MG_Backend::GetFormatCapabilityTargetIndex(textureTarget);
            const SizeT formatIndex = static_cast<SizeT>(textureInternalFormat);
            if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount ||
                formatIndex >= MG_Backend::kFormatCapabilityFormatCount) {
                return 0;
            }
            const auto& sampleCounts =
                MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
            return sampleCounts.empty() ? 0 : sampleCounts.front();
        }

        // The ceiling the frontend enforces, which is EXACTLY the one MobileGL advertises for
        // this format's category - GL_MAX_DEPTH_TEXTURE_SAMPLES, GL_MAX_INTEGER_SAMPLES or
        // GL_MAX_COLOR_TEXTURE_SAMPLES, all three of which have a GL 4.6 minimum of one and are
        // reported as probed. It used to floor all three at GL_MAX_SAMPLES (4) on the reasoning
        // that an application reads GL_MAX_SAMPLES once and hands that count to every
        // glTexStorage*Multisample. That reasoning had it backwards: on Adreno and on Mali an
        // integer multisample texture is backed by ONE sample, so accepting four here did not
        // make four samples exist - ClampSamplesToBackendSupport quietly allocated one and the
        // application wrote per-sample data it could never read back. Raising INVALID_OPERATION
        // is what a real driver does, and it is what makes that silent squeeze unreachable for
        // application-visible storage.
        Int GetMaxSupportedTextureSamples(TextureTarget textureTarget,
                                          TextureInternalFormat textureInternalFormat) {
            if (MG_Backend::pActiveBackendObject == nullptr) {
                return std::numeric_limits<Int>::max();
            }

            const Bool isDepthOrStencil = MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) ||
                                          MG_Util::IsStencilFormatInternalFormat(textureInternalFormat);
            Bool isIntegerFormat = false;
            if (!isDepthOrStencil) {
                GLenum normalizedInternalFormat =
                    MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
                GLenum normalizedFormat = GL_RGBA;
                GLenum normalizedType = GL_UNSIGNED_BYTE;
                MG_Util::TextureFormatProcessor::NormalizePixelFormat(
                    normalizedInternalFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat,
                    &normalizedFormat, &normalizedType);
                isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER ||
                                  normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER;
            }
            const Int categoryMaxSamples = isDepthOrStencil ? GetAdvertisedDepthTextureMaxSamples()
                                           : isIntegerFormat ? GetAdvertisedIntegerMaxSamples()
                                                             : GetAdvertisedColorTextureMaxSamples();

            // glGetInternalformativ(GL_SAMPLES) is answered from this very list
            // (GetInternalformativ below), and GL 4.6 core 8.8 makes that query the definition of
            // the per-format maximum - so when the probe has an answer it IS the ceiling, and the
            // category limit only stands in where nothing was probed.
            //
            // This used to be max(probed, category), which made the probe dead: the walk starts
            // AT the category limit (BackendObject_DirectGLES's ProbeTextureSampleCounts) so its
            // head can never exceed it, and max() therefore always collapsed to the category
            // value. A format whose 4- and 2-sample probes fail inside a 4-sample category - a
            // float colour format under EXT_color_buffer_float is the natural instance - was
            // still accepted at 4, silently squeezed to 1 by ClampSamplesToBackendSupport, and
            // then reported as 4 by GL_TEXTURE_SAMPLES while glGetInternalformativ said 1.
            const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat);
            return probedMaxSamples > 0 ? probedMaxSamples : categoryMaxSamples;
        }

        Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width,
                                               GLsizei height, GLsizei depth, TextureInternalFormat textureInternalFormat,
                                               const char* caller) {
            if (!IsMultisampleTextureTarget(textureTarget)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "Target is not a multisample texture target."));
                return false;
            }
            if (samples <= 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Sample count must be positive."));
                return false;
            }
            if (width < 0 || height < 0 || depth < 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture size must be non-negative."));
                return false;
            }
            if (textureTarget == TextureTarget::Texture2DMultisample && depth != 1) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "2D multisample textures must use depth 1."));
                return false;
            }
            // Zero layers is NOT an error for multisample arrays: depth == 0 (like width/height
            // == 0) deallocates the image - GL 4.5 8.8 only raises INVALID_VALUE for negative
            // dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default
            // GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0).

            const Int maxSamples = GetMaxSupportedTextureSamples(textureTarget, textureInternalFormat);
            if (samples > maxSamples) {
                // GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
                // exceeds what the format supports, and the native Adreno driver agrees.
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", caller,
                        std::format("Sample count {} exceeds the supported maximum {} for this texture format.",
                                    samples, maxSamples)));
                return false;
            }
            return true;
        }

        void AllocateMultisampleTextureStorage(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                               TextureUploadTarget textureUploadTarget,
                                               TextureInternalFormat textureInternalFormat, GLsizei samples,
                                               GLsizei width, GLsizei height, GLsizei depth,
                                               GLboolean fixedsamplelocations) {
            MOBILEGL_ASSERT(textureObject != nullptr, "AllocateMultisampleTextureStorage requires a texture object");
            MOBILEGL_ASSERT(textureObject->GetStorageType() == TextureStorageType::Mipmap,
                            "AllocateMultisampleTextureStorage requires mipmap-backed storage");

            auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
            // GL 4.6 core 8.8: a zero-sized image DEALLOCATES the image rather than defining an
            // empty one. Only the multisample pair cares, and it cares a great deal: the CTS's
            // per-case state reset clears both DEFAULT multisample textures this way on every
            // texture unit, and a "defined" 0x0 default texture stops being skipped by
            // IsUndefinedDefaultTexture - it then joins the per-draw sync and bind passes on
            // every unit the reset touched, and reaches an ES glTexStorage*Multisample(..., 0, 0)
            // that ES 3.1 8.19 makes INVALID_VALUE on every driver there is. A proxy target holds
            // no image at all, only the query result, so it keeps recording what was asked for.
            if ((width <= 0 || height <= 0 || depth <= 0) &&
                !TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
                textureObject->SetInternalFormat(TextureInternalFormat::Unknown);
                textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 0);
                return;
            }
            textureObject->SetInternalFormat(textureInternalFormat);
            textureObject->SetSamples(samples);
            textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
            textureMipmapObject->AllocateStorage(textureUploadTarget, 0, {{width, height, depth}, 0});
            // Multisample textures are single-level by definition, so a name that previously held a
            // mip chain must not keep its tail now that AllocateStorage only grows.
            textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 1);
            textureMipmapObject->MarkStorageDirty(textureUploadTarget, 0, false);
        }

        // Redefining level 0 of a texture that already had a base image drops the rest of the chain,
        // which is exactly what AllocateLevel used to do implicitly for every level. Keeping that
        // behaviour for level 0 - and only for level 0 - is what makes the grow-only change safe:
        // any level-0 respecification leaves the chain in precisely the state it would have had
        // before, while an upload to level N no longer destroys the levels beneath it.
        //
        // Why it has to be *every* level-0 respecification and not just a size change: Minecraft's
        // Mipmap Levels setting rebuilds the block atlas at the SAME dimensions with a different
        // level count. A size-only test would leave the old tail in place, and because Mojang
        // terminates its chains with a 0x0 level the result is the zero-then-nonzero pattern that
        // IsComplete() rejects (TextureObject.cpp) - whereupon DirectGLES skips syncing the texture
        // entirely (Managers.cpp) and the atlas samples black.
        //
        // The "already has a base image" test is what lets the fix work at all: a level that was
        // never written reads back as {0,0,0}, so building a chain top-down - upload level N first,
        // then level 0 - must not discard the levels just uploaded. That ordering is what
        // KHR-GL33.texture_repeat_mode does.
        // Scoped to the respecified upload target only, which is what AllocateLevel already did.
        // Cube maps keep six independent chains while reporting a single level count (face +X), so
        // respecifying a face other than +X can leave the count longer than that face - but that
        // asymmetry predates this change and widening the truncation to all six faces would destroy
        // mip data for faces the application never touched. Left alone deliberately.
        void DiscardMipmapChainOnBaseRespecification(MG_State::GLState::TextureObjectMipmap* texture,
                                                     TextureUploadTarget uploadTarget, Uint level) {
            if (level != 0) return;

            const IntVec3 existingBaseSize = texture->GetMipmapTexelSize(uploadTarget, 0);
            const Bool hasExistingBaseImage =
                existingBaseSize.x() > 0 && existingBaseSize.y() > 0 && existingBaseSize.z() > 0;
            if (!hasExistingBaseImage) return;

            texture->TruncateMipmapLevels(uploadTarget, 1);
        }

        // The compressed internalformat is not one this stack can store (see
        // MG_Util::GetCompressedFormatInfo for the accepted set: the RGTC/BPTC/ETC2-EAC formats core
        // GL requires). GL_INVALID_ENUM is the specified error for an unsupported compressed format -
        // unlike THROW_UNIMPL_EXCEPTION, which unwinds a C++ exception through the C GL ABI and takes
        // the process down. Still the only outcome for the 1D/3D and sub-image entry points, which
        // have no compressed upload path yet.
        void RecordUnsupportedCompressedFormat(const char* caller) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Compressed texture formats are not supported."));
        }

        // GL_TEXTURE_WIDTH of a buffer texture: how many texels of the texture's internal format fit
        // in the buffer range it addresses, CLAMPED to GL_MAX_TEXTURE_BUFFER_SIZE. Attaching a larger
        // buffer is legal (GL 4.6 core 8.9) - the texture simply addresses the first
        // MAX_TEXTURE_BUFFER_SIZE texels of it, and that clamped count is what WIDTH reports.
        //
        // GL_TEXTURE_BUFFER_SIZE is deliberately NOT clamped the same way: it reports the range in
        // basic machine units exactly as glTexBuffer/glTexBufferRange were given it. Swapping the two
        // fails KHR-GL43.texture_buffer.texture_buffer_max_size in the opposite direction.
        GLint GetBufferTextureTexelWidth(const MG_State::GLState::ITextureObject* textureObject) {
            const SizeT texelByteSize = MG_Util::GetSizedInternalFormatSizeInBytes(textureObject->GetFormat());
            // A format with no known footprint has no texel count to report; answering 0 beats
            // dividing by it.
            if (texelByteSize == 0) return 0;
            const auto* bufferTextureObject =
                static_cast<const MG_State::GLState::TextureObjectBuffer*>(textureObject);
            const SizeT texelCount = bufferTextureObject->GetBufferRangeSizeInBytes() / texelByteSize;
            const SizeT maxTexelCount = static_cast<SizeT>(
                std::max(0, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxTextureBufferSize));
            return static_cast<GLint>(std::min(texelCount, maxTexelCount));
        }

        // glGetTexLevelParameter{i,f}v answers WIDTH/HEIGHT/DEPTH out of the mipmap chain, and (since
        // the buffer-texture arms above) out of the attached buffer range for GL_TEXTURE_BUFFER. This
        // is what is left: a storage class with no level geometry at all. Report it instead of
        // throwing - THROW_UNIMPL_EXCEPTION unwinds a C++ exception through the C GL ABI and takes the
        // process down, which is never an acceptable answer to a query - see the same reasoning above
        // for the compressed-format path.
        void RecordUnsupportedLevelQueryStorage(const char* caller, GLenum pname) {
            MGLOG_W_ONCE("%s: glGetTexLevelParameter(pname=%s) is not implemented for this texture's "
                    "storage class; recording GL_INVALID_OPERATION instead of terminating",
                    caller, MG_Util::ConvertGLEnumToString(pname).c_str());
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    "Level queries are not supported for this texture's storage class."));
        }
    } // namespace

    const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
        auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
        if (!textureObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             std::format("Texture object {} does not exist.", texture)));
            return nullTextureObject;
        }
        return textureObject;
    }

    // Whether a raw internalformat enum names a compressed format - the question GL asks whenever an
    // entry point is forbidden on a compressed image: glTexStorage3D on TEXTURE_3D (no
    // block-compressed format is defined for a three-dimensional image, so it is INVALID_OPERATION
    // rather than the INVALID_ENUM an unknown sized format gets - GL 4.6 core 8.19 / Khronos bug
    // 11239, KHR-GLxx.texture_storage.compressed_data) and the clear-texture pair (8.19 again).
    // Written against the enum ranges rather than a name list because the families are contiguous
    // and MobileGL's own internal-format enum drops the ones it cannot carry, which would make this
    // check silently narrower than the API surface.
    static Bool IsCompressedGLInternalFormat(GLenum internalformat) {
        switch (internalformat) {
        case 0x8225: // GL_COMPRESSED_RED
        case 0x8226: // GL_COMPRESSED_RG
        case 0x84ED: // GL_COMPRESSED_RGB
        case 0x84EE: // GL_COMPRESSED_RGBA
        case 0x8C48: // GL_COMPRESSED_SRGB
        case 0x8C49: // GL_COMPRESSED_SRGB_ALPHA
            return true;
        default:
            break;
        }
        return (internalformat >= 0x83F0 && internalformat <= 0x83F3) || // S3TC / DXT
               (internalformat >= 0x8DBB && internalformat <= 0x8DBE) || // RGTC
               (internalformat >= 0x8E8C && internalformat <= 0x8E8F) || // BPTC
               (internalformat >= 0x9270 && internalformat <= 0x9279) || // ETC2 / EAC
               (internalformat >= 0x93B0 && internalformat <= 0x93BD) || // ASTC LDR
               (internalformat >= 0x93D0 && internalformat <= 0x93DD);   // ASTC sRGB
    }

    namespace {
        void RecordClearTextureError(const char* caller, ErrorCode code, const String& message) {
            MG_State::pGLContext->RecordError(
                code, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, message));
        }

        SharedPtr<MG_State::GLState::TextureObjectMipmap> GetClearTextureObject(GLuint texture, GLint level,
                                                                                const char* caller) {
            if (texture == 0) {
                RecordClearTextureError(caller, ErrorCode::InvalidOperation,
                                        "Clear texture operations require a non-zero texture name.");
                return nullptr;
            }

            auto textureObject = GetTextureObjectByName(texture, caller);
            if (!textureObject) return nullptr;
            if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
                RecordClearTextureError(caller, ErrorCode::InvalidOperation,
                                        "Buffer textures cannot be cleared with glClearTexImage.");
                return nullptr;
            }

            auto mipmapTexture = std::static_pointer_cast<MG_State::GLState::TextureObjectMipmap>(textureObject);
            if (level < 0) {
                RecordClearTextureError(caller, ErrorCode::InvalidValue,
                                        std::format("Texture level {} is negative.", level));
                return nullptr;
            }
            // ARB_clear_texture: clearing an image that was never defined by TexImage*/
            // TexStorage* is INVALID_OPERATION, not INVALID_VALUE.
            if (static_cast<Uint>(level) >= mipmapTexture->GetMipmapLevelCount()) {
                RecordClearTextureError(caller, ErrorCode::InvalidOperation,
                                        std::format("Texture level {} is not defined.", level));
                return nullptr;
            }
            // GL 4.6 core 8.19: a compressed internal format is INVALID_OPERATION for both clear
            // entry points. Two tags to ask, because they answer different questions: the stored
            // one covers a level glCompressedTexImage* or a SPECIFIC compressed internalformat
            // defined, the requested one covers the six generic GL_COMPRESSED_* enums that MobileGL
            // deliberately backs with uncompressed storage (see MipmapStorage) and that would
            // otherwise look like an ordinary RGBA8 image by the time the clear runs.
            const auto& uploadTargets = mipmapTexture->GetUploadTargets();
            if (!uploadTargets.empty() &&
                (mipmapTexture->GetMipmapCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) != GL_NONE ||
                 mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTargets[0], static_cast<Uint>(level)) !=
                     GL_NONE)) {
                RecordClearTextureError(caller, ErrorCode::InvalidOperation,
                                        "Compressed textures cannot be cleared.");
                return nullptr;
            }
            return mipmapTexture;
        }

        Bool BuildClearPixel(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
                             GLenum format, GLenum type, const void* data, Vector<Uint8>& clearPixel) {
            const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
            const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
            if (!TextureImpl::ValidateTextureInputFormat(inputFormat) ||
                !TextureImpl::ValidateTexturePixelDataType(inputType) ||
                !TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
                    inputFormat, textureObject->GetFormat(), inputType)) {
                return false;
            }

            clearPixel.clear();
            if (data == nullptr) {
                // ARB_clear_texture defines a null clear value as all zeroes. Keeping the
                // pattern empty lets the region writer use a fast memset path.
                return true;
            }

            PixelStoreParameters clearPixelStore{};
            clearPixelStore.Alignment = 1;
            SizeT clearPixelSize = 0;
            void* converted = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
                data, clearPixelStore, textureObject->GetFormat(), inputFormat, inputType,
                {1, 1, 1}, false, clearPixelSize);
            if (!converted || clearPixelSize == 0) {
                if (converted) free(converted);
                return false;
            }

            clearPixel.resize(clearPixelSize);
            Memcpy(clearPixel.data(), converted, clearPixelSize);
            free(converted);
            return true;
        }

        // Writes the clear into the CPU shadow and marks the whole level dirty, exactly like
        // TexSubImage*_State does. Shared limitation of the level-granular shadow sync: the
        // shadow does not reflect GPU-side writes (FBO rendering, imageStore), so a PARTIAL
        // clear of a GPU-written level re-uploads stale shadow bytes outside the region on
        // the next sync. Full-level clears (glClearTexImage, or a sub-clear covering the
        // level) rewrite the entire shadow and are always correct.
        Bool ClearMipmapRegion(const SharedPtr<MG_State::GLState::TextureObjectMipmap>& textureObject,
                               TextureUploadTarget uploadTarget, GLint level,
                               GLint xoffset, GLint yoffset, GLint zoffset,
                               GLsizei width, GLsizei height, GLsizei depth,
                               const Vector<Uint8>& clearPixel, const char* caller) {
            const IntVec3 texelSize = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
            if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
                RecordClearTextureError(caller, ErrorCode::InvalidOperation,
                                        "The requested texture level has no storage.");
                return false;
            }
            if (xoffset < 0 || yoffset < 0 || zoffset < 0 ||
                width < 0 || height < 0 || depth < 0 ||
                width > texelSize.x() - xoffset ||
                height > texelSize.y() - yoffset ||
                depth > texelSize.z() - zoffset) {
                RecordClearTextureError(caller, ErrorCode::InvalidValue,
                                        "The clear region lies outside the requested texture level.");
                return false;
            }
            if (width == 0 || height == 0 || depth == 0) return true;

            const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
                                     static_cast<SizeT>(texelSize.y()) *
                                     static_cast<SizeT>(texelSize.z());
            const SizeT byteSize = textureObject->GetMipmapByteSize(uploadTarget, static_cast<Uint>(level));
            if (byteSize == 0 || byteSize % texelCount != 0) {
                RecordClearTextureError(caller, ErrorCode::InvalidOperation,
                                        "The requested texture storage cannot be cleared.");
                return false;
            }

            const SizeT bytesPerTexel = byteSize / texelCount;
            if (!clearPixel.empty() && clearPixel.size() != bytesPerTexel) {
                RecordClearTextureError(
                    caller, ErrorCode::InvalidOperation,
                    std::format("Converted clear value is {} bytes, but the texture stores {} bytes per texel.",
                                clearPixel.size(), bytesPerTexel));
                return false;
            }

            auto* destination = static_cast<Uint8*>(
                textureObject->MapMipmapData(uploadTarget, static_cast<Uint>(level)));
            if (!destination) {
                RecordClearTextureError(caller, ErrorCode::InvalidOperation,
                                        "The requested texture level could not be mapped.");
                return false;
            }

            const SizeT fullRowBytes = static_cast<SizeT>(texelSize.x()) * bytesPerTexel;
            const SizeT fullSliceBytes = static_cast<SizeT>(texelSize.y()) * fullRowBytes;
            const SizeT clearRowBytes = static_cast<SizeT>(width) * bytesPerTexel;
            Uint8* firstClearRow = nullptr;

            for (GLsizei z = 0; z < depth; ++z) {
                for (GLsizei y = 0; y < height; ++y) {
                    Uint8* row = destination +
                                 static_cast<SizeT>(zoffset + z) * fullSliceBytes +
                                 static_cast<SizeT>(yoffset + y) * fullRowBytes +
                                 static_cast<SizeT>(xoffset) * bytesPerTexel;
                    if (firstClearRow) {
                        Memcpy(row, firstClearRow, clearRowBytes);
                        continue;
                    }

                    firstClearRow = row;
                    if (clearPixel.empty()) {
                        Memset(row, 0, clearRowBytes);
                        continue;
                    }

                    Memcpy(row, clearPixel.data(), bytesPerTexel);
                    SizeT filled = bytesPerTexel;
                    while (filled < clearRowBytes) {
                        const SizeT copySize = std::min(filled, clearRowBytes - filled);
                        Memcpy(row + filled, row, copySize);
                        filled += copySize;
                    }
                }
            }

            textureObject->MarkStorageDirty(uploadTarget, static_cast<Uint>(level), true);
            return true;
        }
        // GL 4.6 core 8.6: CopyTexSubImage* is not affected by pixel-store state or by a bound
        // pack buffer, but the backend readback this borrows honours both. Neutralise them for the
        // duration of the read and put them back afterwards.
        class ScopedNeutralPackState {
        public:
            ScopedNeutralPackState() {
                for (SizeT i = 0; i < kParams.size(); ++i) {
                    m_saved[i] = MG_State::pGLContext->GetPixelStoreParam(kParams[i]);
                    MG_State::pGLContext->SetPixelStoreParam(kParams[i], i == 0 ? 1 : 0);
                }
                auto& slot = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack);
                m_savedPackBuffer = slot.GetBoundObject();
                slot.Bind(nullptr);
            }
            ~ScopedNeutralPackState() {
                for (SizeT i = 0; i < kParams.size(); ++i) {
                    MG_State::pGLContext->SetPixelStoreParam(kParams[i], m_saved[i]);
                }
                MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).Bind(m_savedPackBuffer);
            }

        private:
            // PackAlignment must be first: it is the one that resets to 1 rather than 0.
            static constexpr Array<PixelStoreParam, 8> kParams{
                PixelStoreParam::PackAlignment,  PixelStoreParam::PackRowLength,
                PixelStoreParam::PackImageHeight, PixelStoreParam::PackSkipRows,
                PixelStoreParam::PackSkipPixels, PixelStoreParam::PackSkipImages,
                PixelStoreParam::PackSwapBytes,  PixelStoreParam::PackLSBFirst};
            Array<Int, 8> m_saved{};
            SharedPtr<MG_State::GLState::BufferObject> m_savedPackBuffer;
        };

        // The copy half of glCopyTexSubImage*: read the region out of the read framebuffer and write
        // it into the destination level's CPU storage. Done in the frontend because that storage is
        // where a texture's contents actually live - the backends sync from it - so this needs no
        // 1D or 3D blit, which neither backend has.
        Bool CopyReadFramebufferIntoMipmapRegion(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                                 TextureUploadTarget uploadTarget, GLint level, GLint xoffset,
                                                 GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width,
                                                 GLsizei height, const char* caller) {
            if (width <= 0 || height <= 0) return true;
            auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
            if (!mipmapTexture) return false;

            const auto texelSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
            const SizeT texelCount = static_cast<SizeT>(texelSize.x()) * static_cast<SizeT>(texelSize.y()) *
                                     static_cast<SizeT>(texelSize.z());
            const SizeT byteSize = mipmapTexture->GetMipmapByteSize(uploadTarget, static_cast<Uint>(level));
            if (texelCount == 0 || byteSize == 0 || byteSize % texelCount != 0) return false;
            const SizeT bytesPerTexel = byteSize / texelCount;

            // Read in the destination's own canonical client layout, so the bytes land in storage
            // without a second conversion.
            const GLenum glInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
            GLenum realInternalFormat = glInternalFormat;
            GLenum format = GL_RGBA;
            GLenum type = GL_UNSIGNED_BYTE;
            MG_Util::TextureFormatProcessor::NormalizePixelFormat(glInternalFormat, PixelFormatNormalizeOptionBit::None,
                                                                  &realInternalFormat, &format, &type);
            const SizeT readBytesPerTexel =
                MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(format),
                                               MG_Util::ConvertGLEnumToTexturePixelDataType(type));
            if (readBytesPerTexel != bytesPerTexel) {
                MGLOG_W_ONCE("%s: cannot copy into a %zu-byte texel from a %zu-byte readback layout", caller,
                        bytesPerTexel, readBytesPerTexel);
                return false;
            }

            Vector<Uint8> scratch(static_cast<SizeT>(width) * static_cast<SizeT>(height) * bytesPerTexel);
            {
                ScopedNeutralPackState neutralPack;
                MG_Backend::gBackendFunctionsTable.GL.ReadPixels(x, y, width, height, format, type, scratch.data());
            }

            auto* destination =
                static_cast<Uint8*>(mipmapTexture->MapMipmapData(uploadTarget, static_cast<Uint>(level)));
            if (!destination) return false;

            const SizeT fullRowBytes = static_cast<SizeT>(texelSize.x()) * bytesPerTexel;
            const SizeT fullSliceBytes = static_cast<SizeT>(texelSize.y()) * fullRowBytes;
            const SizeT copyRowBytes = static_cast<SizeT>(width) * bytesPerTexel;
            for (GLsizei row = 0; row < height; ++row) {
                Uint8* dst = destination + static_cast<SizeT>(zoffset) * fullSliceBytes +
                             static_cast<SizeT>(yoffset + row) * fullRowBytes +
                             static_cast<SizeT>(xoffset) * bytesPerTexel;
                Memcpy(dst, scratch.data() + static_cast<SizeT>(row) * copyRowBytes, copyRowBytes);
            }
            mipmapTexture->MarkStorageDirty(uploadTarget, static_cast<Uint>(level), true);
            return true;
        }
    } // namespace

    void ClearTexImage(GLuint texture, GLint level, GLenum format, GLenum type, const void* data) {
        auto textureObject = GetClearTextureObject(texture, level, __func__);
        if (!textureObject) return;

        Vector<Uint8> clearPixel;
        if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;

        for (TextureUploadTarget uploadTarget : textureObject->GetUploadTargets()) {
            const IntVec3 size = textureObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
            if (!ClearMipmapRegion(textureObject, uploadTarget, level, 0, 0, 0,
                                   size.x(), size.y(), size.z(), clearPixel, __func__)) {
                return;
            }
        }
    }

    void ClearTexSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
                          GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type,
                          const void* data) {
        auto textureObject = GetClearTextureObject(texture, level, __func__);
        if (!textureObject) return;

        Vector<Uint8> clearPixel;
        if (!BuildClearPixel(textureObject, format, type, data, clearPixel)) return;

        const auto& uploadTargets = textureObject->GetUploadTargets();
        if (textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
            if (zoffset < 0 || depth < 0 ||
                static_cast<SizeT>(zoffset) > uploadTargets.size() ||
                static_cast<SizeT>(depth) > uploadTargets.size() - static_cast<SizeT>(zoffset)) {
                RecordClearTextureError(__func__, ErrorCode::InvalidValue,
                                        "The cube-map clear region selects invalid faces.");
                return;
            }
            for (GLsizei face = 0; face < depth; ++face) {
                if (!ClearMipmapRegion(textureObject, uploadTargets[static_cast<SizeT>(zoffset + face)], level,
                                       xoffset, yoffset, 0, width, height, 1, clearPixel, __func__)) {
                    return;
                }
            }
            return;
        }

        if (uploadTargets.empty()) {
            RecordClearTextureError(__func__, ErrorCode::InvalidOperation,
                                    "The requested texture has no upload target.");
            return;
        }
        ClearMipmapRegion(textureObject, uploadTargets.front(), level, xoffset, yoffset, zoffset,
                          width, height, depth, clearPixel, __func__);
    }

    Bool ValidateTextureParameterForTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                           GLenum pname, GLint param, const char* caller) {
        const auto target = textureObject->GetTarget();
        if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) {
            return false;
        }
        if ((pname == GL_TEXTURE_BASE_LEVEL || pname == GL_TEXTURE_MAX_LEVEL) && param < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level parameter must be non-negative."));
            return false;
        }

        if ((target == TextureTarget::Texture2DMultisample ||
             target == TextureTarget::Texture2DMultisampleArray) &&
            pname == GL_TEXTURE_BASE_LEVEL && param != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Multisample texture base level must be zero."));
            return false;
        }

        if (target == TextureTarget::TextureRectangle && pname == GL_TEXTURE_BASE_LEVEL && param != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Rectangle texture base level must be zero."));
            return false;
        }

        if ((target == TextureTarget::Texture2DMultisample ||
             target == TextureTarget::Texture2DMultisampleArray) &&
            (pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T || pname == GL_TEXTURE_WRAP_R ||
             pname == GL_TEXTURE_MIN_FILTER || pname == GL_TEXTURE_MAG_FILTER || pname == GL_TEXTURE_MIN_LOD ||
             pname == GL_TEXTURE_MAX_LOD || pname == GL_TEXTURE_LOD_BIAS || pname == GL_TEXTURE_COMPARE_MODE ||
             pname == GL_TEXTURE_COMPARE_FUNC || pname == GL_TEXTURE_BORDER_COLOR ||
             pname == GL_TEXTURE_MAX_ANISOTROPY_EXT)) {
            // GL 4.6 core 8.10: a multisample target simply does not ACCEPT these pnames, which is
            // an INVALID_ENUM - not the INVALID_OPERATION the two BASE_LEVEL gates above report.
            // Those really are operation errors (the pname is accepted, the value is not), which is
            // presumably how the wrong class got copied down here.
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Multisample textures do not accept sampler-state pnames."));
            return false;
        }

        // The six pnames a texture object shares with a sampler object carry an enum VALUE, and an
        // unrecognised one is INVALID_ENUM. The texture path used to hand the value straight to
        // ConvertGLEnumToSamplerWrapMode / ...FilterMode and throw the Unknown away, so
        // glTexParameteri(GL_TEXTURE_WRAP_S, GL_RED) was silently accepted. Sampler objects have had
        // exactly this validator all along; calling it here rather than writing a second one is also
        // what keeps the two spellings of the same state from drifting.
        //
        // Called selectively: ValidateSamplerParam's default arm reports InvalidEnum for anything it
        // does not know, and the texture-only pnames (BASE_LEVEL, SWIZZLE_*, ...) are not in its list.
        switch (pname) {
        case GL_TEXTURE_WRAP_S:
        case GL_TEXTURE_WRAP_T:
        case GL_TEXTURE_WRAP_R:
        case GL_TEXTURE_MIN_FILTER:
        case GL_TEXTURE_MAG_FILTER:
        case GL_TEXTURE_COMPARE_MODE:
        case GL_TEXTURE_COMPARE_FUNC:
            if (!SamplerImpl::ValidateSamplerParam(pname, static_cast<GLenum>(param))) return false;
            break;
        default:
            break;
        }

        if (target == TextureTarget::TextureRectangle) {
            if ((pname == GL_TEXTURE_WRAP_S || pname == GL_TEXTURE_WRAP_T) &&
                (param == GL_MIRROR_CLAMP_TO_EDGE || param == GL_MIRRORED_REPEAT || param == GL_REPEAT)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "Invalid wrap mode for rectangle texture."));
                return false;
            }
            if (pname == GL_TEXTURE_MIN_FILTER && param != GL_NEAREST && param != GL_LINEAR) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "Invalid min filter for rectangle texture."));
                return false;
            }
        }

        return true;
    }

    TextureUploadTarget GetPrimaryUploadTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
        if (!textureObject) return TextureUploadTarget::Unknown;
        const auto& uploadTargets = textureObject->GetUploadTargets();
        return uploadTargets.empty() ? TextureUploadTarget::Unknown : uploadTargets[0];
    }

    void TextureParameterObject_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
                                      GLint param, const char* caller) {
        if (!textureObject) return;
        if (!ValidateTextureParameterForTarget(textureObject, pname, param, caller)) return;

        switch (pname) {
        case GL_TEXTURE_MAG_FILTER:
            textureObject->GetSamplerObject()->SetMagFilter(MG_Util::ConvertGLEnumToSamplerFilterMode(param));
            break;
        case GL_TEXTURE_MIN_FILTER:
            textureObject->GetSamplerObject()->SetMinFilter(MG_Util::ConvertGLEnumToSamplerFilterMode(param));
            textureObject->GetSamplerObject()->SetMipmapMode(MG_Util::ConvertGLEnumToSamplerMipmapMode(param));
            break;
        case GL_TEXTURE_MIN_LOD: {
            Float maxLod = textureObject->GetSamplerObject()->GetMaxLod();
            textureObject->GetSamplerObject()->SetLodRange(param, maxLod);
            break;
        }
        case GL_TEXTURE_MAX_LOD: {
            Float minLod = textureObject->GetSamplerObject()->GetMinLod();
            textureObject->GetSamplerObject()->SetLodRange(minLod, param);
            break;
        }
        case GL_TEXTURE_BASE_LEVEL:
            textureObject->SetBaseLevel(param);
            break;
        case GL_TEXTURE_MAX_LEVEL:
            textureObject->SetMaxLevel(param);
            break;
        case GL_TEXTURE_SWIZZLE_R:
        case GL_TEXTURE_SWIZZLE_G:
        case GL_TEXTURE_SWIZZLE_B:
        case GL_TEXTURE_SWIZZLE_A: {
            auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
            auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam(param);
            if (swizzleValue == TextureSwizzleParam::Unknown) {
                // GL CTS texture_swizzle.api_errors: single-value TexParameter* with a value outside
                // [RED, GREEN, BLUE, ALPHA, ZERO, ONE] must raise GL_INVALID_ENUM.
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Invalid texture swizzle value."));
                return;
            }
            textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
            break;
        }
        case GL_TEXTURE_WRAP_S:
            textureObject->GetSamplerObject()->SetWrapS(MG_Util::ConvertGLEnumToSamplerWrapMode(param));
            break;
        case GL_TEXTURE_WRAP_T:
            textureObject->GetSamplerObject()->SetWrapT(MG_Util::ConvertGLEnumToSamplerWrapMode(param));
            break;
        case GL_TEXTURE_WRAP_R:
            textureObject->GetSamplerObject()->SetWrapR(MG_Util::ConvertGLEnumToSamplerWrapMode(param));
            break;
        case GL_TEXTURE_COMPARE_MODE:
            textureObject->GetSamplerObject()->SetCompareMode(MG_Util::ConvertGLEnumToSamplerCompareMode(param));
            break;
        case GL_TEXTURE_COMPARE_FUNC:
            textureObject->GetSamplerObject()->SetSamplerCompareFunc(MG_Util::ConvertGLEnumToSamplerCompareFunc(param));
            break;
        case GL_TEXTURE_LOD_BIAS:
            textureObject->GetSamplerObject()->SetLodBias((GLfloat)param);
            break;
        case GL_TEXTURE_MAX_ANISOTROPY_EXT:
            textureObject->GetSamplerObject()->SetMaxAnisotropy(static_cast<GLfloat>(param));
            break;
        case GL_GENERATE_MIPMAP:
            g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != GL_FALSE);
            break;
        case GL_DEPTH_STENCIL_TEXTURE_MODE:
            if (param != GL_DEPTH_COMPONENT && param != GL_STENCIL_INDEX) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "Invalid GL_DEPTH_STENCIL_TEXTURE_MODE value."));
                return;
            }
            textureObject->SetDepthStencilTextureMode(static_cast<GLenum>(param));
            break;
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
            return;
        }
    }

    void TextureParameterObjectf_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLenum pname,
                                       GLfloat param, const char* caller) {
        if (!textureObject) return;
        if (pname == GL_TEXTURE_MAX_ANISOTROPY_EXT && !ValidateMaxAnisotropy(param, caller)) return;
        const GLint validationParam =
            pname == GL_TEXTURE_MAX_ANISOTROPY_EXT ? 1 : static_cast<GLint>(param);
        if (!ValidateTextureParameterForTarget(textureObject, pname, validationParam, caller)) return;

        switch (pname) {
        case GL_TEXTURE_MAG_FILTER:
            textureObject->GetSamplerObject()->SetMagFilter(MG_Util::ConvertGLEnumToSamplerFilterMode((GLenum)param));
            break;
        case GL_TEXTURE_MIN_FILTER:
            textureObject->GetSamplerObject()->SetMinFilter(MG_Util::ConvertGLEnumToSamplerFilterMode((GLenum)param));
            textureObject->GetSamplerObject()->SetMipmapMode(MG_Util::ConvertGLEnumToSamplerMipmapMode((GLenum)param));
            break;
        case GL_TEXTURE_MIN_LOD: {
            Float maxLod = textureObject->GetSamplerObject()->GetMaxLod();
            textureObject->GetSamplerObject()->SetLodRange(param, maxLod);
            break;
        }
        case GL_TEXTURE_MAX_LOD: {
            Float minLod = textureObject->GetSamplerObject()->GetMinLod();
            textureObject->GetSamplerObject()->SetLodRange(minLod, param);
            break;
        }
        case GL_TEXTURE_BASE_LEVEL:
            textureObject->SetBaseLevel((Uint)param);
            break;
        case GL_TEXTURE_MAX_LEVEL:
            textureObject->SetMaxLevel((Uint)param);
            break;
        case GL_TEXTURE_WRAP_S:
            textureObject->GetSamplerObject()->SetWrapS(MG_Util::ConvertGLEnumToSamplerWrapMode((GLenum)param));
            break;
        case GL_TEXTURE_WRAP_T:
            textureObject->GetSamplerObject()->SetWrapT(MG_Util::ConvertGLEnumToSamplerWrapMode((GLenum)param));
            break;
        case GL_TEXTURE_WRAP_R:
            textureObject->GetSamplerObject()->SetWrapR(MG_Util::ConvertGLEnumToSamplerWrapMode((GLenum)param));
            break;
        case GL_TEXTURE_COMPARE_MODE:
            textureObject->GetSamplerObject()->SetCompareMode(
                MG_Util::ConvertGLEnumToSamplerCompareMode((GLenum)param));
            break;
        case GL_TEXTURE_COMPARE_FUNC:
            textureObject->GetSamplerObject()->SetSamplerCompareFunc(
                MG_Util::ConvertGLEnumToSamplerCompareFunc((GLenum)param));
            break;
        case GL_TEXTURE_LOD_BIAS:
            textureObject->GetSamplerObject()->SetLodBias(param);
            break;
        case GL_TEXTURE_MAX_ANISOTROPY_EXT:
            textureObject->GetSamplerObject()->SetMaxAnisotropy(param);
            break;
        case GL_GENERATE_MIPMAP:
            g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f);
            break;
        case GL_DEPTH_STENCIL_TEXTURE_MODE:
            if (param != GL_DEPTH_COMPONENT && param != GL_STENCIL_INDEX) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "Invalid GL_DEPTH_STENCIL_TEXTURE_MODE value."));
                return;
            }
            textureObject->SetDepthStencilTextureMode(static_cast<GLenum>(param));
            break;
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
            return;
        }
    }

    void GetTextureParameterObjectiv_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                           GLenum pname, GLint* params, const char* caller) {
        if (!textureObject || !params) return;

        switch (pname) {
        case GL_TEXTURE_MAG_FILTER:
            *params = (GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(textureObject->GetSamplerObject()->GetMagFilter(),
                                                                       SamplerMipmapMode::None);
            break;
        case GL_TEXTURE_MIN_FILTER:
            *params = (GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(
                textureObject->GetSamplerObject()->GetMinFilter(), textureObject->GetSamplerObject()->GetMipmapMode());
            break;
        case GL_TEXTURE_MIN_LOD:
            *params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMinLod());
            break;
        case GL_TEXTURE_MAX_LOD:
            *params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMaxLod());
            break;
        case GL_TEXTURE_BASE_LEVEL:
            *params = static_cast<GLint>(textureObject->GetLevelRange().x());
            break;
        case GL_TEXTURE_MAX_LEVEL:
            *params = static_cast<GLint>(textureObject->GetLevelRange().y());
            break;
        case GL_TEXTURE_IMMUTABLE_FORMAT:
            *params = textureObject->IsImmutable() ? GL_TRUE : GL_FALSE;
            break;
        case GL_TEXTURE_IMMUTABLE_LEVELS:
            *params = static_cast<GLint>(textureObject->GetImmutableLevels());
            break;
        case GL_TEXTURE_WRAP_S:
            *params = (GLint)MG_Util::ConvertSamplerWrapModeToGLEnum(textureObject->GetSamplerObject()->GetWrapS());
            break;
        case GL_TEXTURE_WRAP_T:
            *params = (GLint)MG_Util::ConvertSamplerWrapModeToGLEnum(textureObject->GetSamplerObject()->GetWrapT());
            break;
        case GL_TEXTURE_WRAP_R:
            *params = (GLint)MG_Util::ConvertSamplerWrapModeToGLEnum(textureObject->GetSamplerObject()->GetWrapR());
            break;
        case GL_TEXTURE_COMPARE_MODE:
            *params =
                (GLint)MG_Util::ConvertSamplerCompareModeToGLEnum(textureObject->GetSamplerObject()->GetCompareMode());
            break;
        case GL_TEXTURE_COMPARE_FUNC:
            *params = (GLint)MG_Util::ConvertSamplerCompareFuncToGLEnum(
                textureObject->GetSamplerObject()->GetSamplerCompareFunc());
            break;
        case GL_TEXTURE_MAX_ANISOTROPY_EXT:
            *params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMaxAnisotropy());
            break;
        case GL_DEPTH_STENCIL_TEXTURE_MODE:
            *params = static_cast<GLint>(textureObject->GetDepthStencilTextureMode());
            break;
        case GL_TEXTURE_LOD_BIAS:
            *params = static_cast<GLint>(textureObject->GetSamplerObject()->GetLodBias());
            break;
        case GL_TEXTURE_SWIZZLE_R:
        case GL_TEXTURE_SWIZZLE_G:
        case GL_TEXTURE_SWIZZLE_B:
        case GL_TEXTURE_SWIZZLE_A: {
            const auto component = static_cast<SizeT>(pname - GL_TEXTURE_SWIZZLE_R);
            *params = static_cast<GLint>(
                MG_Util::ConvertTextureSwizzleParamToGLEnum(textureObject->GetAllSwizzleParams()[component]));
            break;
        }
        case GL_TEXTURE_TARGET:
            *params = static_cast<GLint>(MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget()));
            break;
        case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
            *params = GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE;
            break;
        // GL 4.6 core table 23.17. All four start at 0 and stay there on a mutable texture;
        // TexStorage* seeds them with the texture's full extent and glTextureView composes onto
        // that (see SeedImmutableViewState and TextureView).
        case GL_TEXTURE_VIEW_MIN_LEVEL:
            *params = static_cast<GLint>(textureObject->GetViewMinLevel());
            break;
        case GL_TEXTURE_VIEW_MIN_LAYER:
            *params = static_cast<GLint>(textureObject->GetViewMinLayer());
            break;
        case GL_TEXTURE_VIEW_NUM_LEVELS:
            *params = static_cast<GLint>(textureObject->GetViewNumLevels());
            break;
        case GL_TEXTURE_VIEW_NUM_LAYERS:
            *params = static_cast<GLint>(textureObject->GetViewNumLayers());
            break;
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    std::format("pname {} is not a valid texture parameter.",
                                MG_Util::ConvertGLEnumToString(pname))));
            return;
        }
    }

    const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByTarget(
        TextureUploadTarget textureUploadTarget, TextureTarget textureTarget) {
        if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
            auto& textureObject =
                TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget);
            if (!TextureImpl::ValidateTextureObject(textureObject)) return nullTextureObject;
            return textureObject;
        } else {
            auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
            auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
            auto& textureObject = bindingSlot.GetBoundObject();
            if (!TextureImpl::ValidateTextureObject(textureObject)) return nullTextureObject;
            return textureObject;
        }
    }

    // The targets glTexParameter* / glGetTexParameter* accept (GL 4.6 core 8.10 and 8.11). This is a
    // SHORTER list than the one ConvertGLEnumToTextureTarget knows, and deliberately so: that
    // converter folds the six cube-map FACE targets onto TextureCubeMap because glTexImage2D and
    // glCopyTexImage2D need exactly that folding, and it maps GL_TEXTURE_BUFFER to a real target
    // because glTexBuffer needs it. Neither is a legal parameter target, so without a separate
    // predicate glTexParameteri(GL_TEXTURE_CUBE_MAP_POSITIVE_X, ...) quietly applied the parameter
    // to the bound cube map and glGetTexParameterIiv(GL_TEXTURE_BUFFER, ...) quietly answered from
    // the default texture - both GL_NO_ERROR where the spec says GL_INVALID_ENUM.
    //
    // An enum the converter does not know at all was equally silent: it produced TextureTarget::
    // Unknown, GetTextureObjectByTargetForParameter handed back the null object and every caller
    // returned without recording anything. Rejecting here closes that too, at the entry point rather
    // than at the lookup, so exactly one error is recorded.
    //
    // EXACTLY the ten targets 8.10 and 8.11 enumerate - no proxies. The spec's own asymmetry is the
    // proof: GetTexLevelParameter needs an explicit clause extending its list with PROXY_TEXTURE_1D,
    // PROXY_TEXTURE_2D and the rest, and neither TexParameter nor GetTexParameter carries one. That
    // clause is why GetTexLevelParameteriv_State/GetTexLevelParameterfv_State are deliberately NOT
    // gated by this predicate.
    //
    // Routing was not a reason to accept them: GetTextureObjectByTargetForParameter resolves a proxy
    // object only after a proxy glTexImage has run, so before that the parameter call was a silent
    // no-op and after it the parameter was applied for real - both GL_NO_ERROR, and both the same
    // silent-acceptance shape this predicate exists to close for cube faces and GL_TEXTURE_BUFFER.
    static Bool IsLegalTextureParameterTarget(GLenum target) {
        switch (target) {
        case GL_TEXTURE_1D:
        case GL_TEXTURE_2D:
        case GL_TEXTURE_3D:
        case GL_TEXTURE_1D_ARRAY:
        case GL_TEXTURE_2D_ARRAY:
        case GL_TEXTURE_RECTANGLE:
        case GL_TEXTURE_CUBE_MAP:
        case GL_TEXTURE_CUBE_MAP_ARRAY:
        case GL_TEXTURE_2D_MULTISAMPLE:
        case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
            return true;
        default:
            return false;
        }
    }

    // The by-NAME spelling of the same rule. glTextureParameter* has no target token, so GL 4.6 core
    // 8.10 applies the list to the texture's EFFECTIVE target instead. The four vector DSA forms
    // reach the gate above for free because they re-enter through WithTemporarilyBoundNamedTexture,
    // which synthesizes the target from the object; the two scalar forms call the per-object setter
    // directly and reached no gate at all, so glTextureParameteri on a buffer texture applied state
    // with GL_NO_ERROR while glTextureParameteriv on the same texture answered GL_INVALID_ENUM.
    static Bool ValidateNamedTextureParameterTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                                    const char* caller) {
        if (!textureObject) return false;
        const GLenum effectiveTarget = MG_Util::ConvertTextureTargetToGLEnum(textureObject->GetTarget());
        if (IsLegalTextureParameterTarget(effectiveTarget)) return true;
        MG_State::pGLContext->RecordError(
            ErrorCode::InvalidEnum,
            MakeUnique<GenericErrorInfo>(
                "MG_Impl/GLImpl", caller,
                std::format("The effective target {} does not accept texture parameters.",
                            MG_Util::ConvertGLEnumToString(effectiveTarget))));
        return false;
    }

    static Bool ValidateTextureParameterTarget(GLenum target, const char* caller) {
        if (IsLegalTextureParameterTarget(target)) return true;
        MG_State::pGLContext->RecordError(
            ErrorCode::InvalidEnum,
            MakeUnique<GenericErrorInfo>(
                "MG_Impl/GLImpl", caller,
                std::format("target {} does not accept texture parameters.", MG_Util::ConvertGLEnumToString(target))));
        return false;
    }

    // Texture-parameter lookups must not raise GL_INVALID_OPERATION when the default texture
    // (name 0) is bound: glTexParameter* on default textures is legal GL (the GL CTS state reset
    // sets swizzles/levels on texture 0 for every unit x target and expects glGetError() to stay
    // clean). Name 0 resolves to the target's real default texture object, so parameters set on
    // it are stored and queryable like on any texture.
    const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByTargetForParameter(
        TextureUploadTarget textureUploadTarget, TextureTarget textureTarget) {
        if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
            return TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget);
        }
        if (textureTarget == TextureTarget::Unknown) {
            return nullTextureObject;
        }
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        return activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
    }

    void GenerateMipmap_Backend(GLenum target) {
        MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap(target);
    }

    void MaybeAutoGenerateMipmap(GLenum target, const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                 Bool isProxy, GLint level) {
        if (isProxy || level != 0 || !textureObject) {
            return;
        }
        const auto it = g_autoGenerateMipmapByTextureId.find(textureObject->GetExternalIndex());
        if (it == g_autoGenerateMipmapByTextureId.end() || !it->second) {
            return;
        }
        GenerateMipmap_Backend(target);
    }

    // GL 4.6 core 8.5: sourcing an upload from a bound PIXEL_UNPACK_BUFFER adds three
    // INVALID_OPERATION conditions that do not exist for client memory. `pixels` is a byte offset
    // into that buffer, not a pointer. Returns true when no unpack buffer is bound, so every caller
    // can run it unconditionally.
    Bool ValidatePixelUnpackBufferSource(const void* pixels, TextureInputFormat inputFormat,
                                         TexturePixelDataType dataType, IntVec3 dimension, const char* caller) {
        const auto& unpackBuffer =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (!unpackBuffer) return true;

        // Persistent mappings remain legal transfer sources, as on the pack side in ReadPixels.
        if (unpackBuffer->IsMapped() && !(unpackBuffer->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel unpack buffer is currently mapped."));
            return false;
        }

        const SizeT offset = reinterpret_cast<SizeT>(pixels);
        const SizeT typeSize = MG_Util::GetTexturePixelDataTypeSize(dataType);
        if (typeSize != 0 && (offset % typeSize) != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Pixel unpack buffer offset must be a multiple of the size of a datum "
                                             "of the given type."));
            return false;
        }

        // The tightly packed span is the smallest the unpack can read, so a request that overruns
        // even this one certainly overruns the store; pixel store parameters only ever widen it.
        const SizeT bufferSize = unpackBuffer->GetSize();
        const SizeT required = MG_Util::CalculateInputTextureImageSize(inputFormat, dataType, dimension);
        if (offset > bufferSize || required > bufferSize - offset) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Unpacking would read past the end of the pixel unpack buffer."));
            return false;
        }

        return true;
    }

    // The same rules for the COMPRESSED entry points, whose payload size is the imageSize the
    // caller passed rather than something derived from a (format, type) pair - and which have no
    // datum size, so the alignment rule above does not apply to them. Shared by
    // glCompressedTexImage2D and glCompressedTexSubImage2D so the two cannot drift; the point
    // that is easy to get wrong and that KHR-GL44.buffer_storage.map_persistent_texture exists to
    // check is the first one: a PERSISTENT mapping stays a legal transfer source.
    Bool ValidateCompressedUnpackBufferSource(const void* data, SizeT imageSize, const char* caller) {
        const auto& unpackBuffer =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (!unpackBuffer) return true;

        if (unpackBuffer->IsMapped() && !(unpackBuffer->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel unpack buffer is currently mapped."));
            return false;
        }

        const SizeT offset = reinterpret_cast<SizeT>(data);
        const SizeT bufferSize = unpackBuffer->GetSize();
        if (offset > bufferSize || imageSize > bufferSize - offset) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Unpacking would read past the end of the pixel unpack buffer."));
            return false;
        }
        return true;
    }

    // Where a compressed upload reads its blocks from: `data` is an offset into the bound unpack
    // buffer when there is one, and a client pointer otherwise. Only meaningful once
    // ValidateCompressedUnpackBufferSource has passed. Null means there is nothing to read, which
    // GL leaves undefined and which callers must not dereference.
    const void* CompressedUnpackSource(const void* data) {
        const auto& unpackBuffer =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (!unpackBuffer) return data;
        return reinterpret_cast<const char*>(unpackBuffer->MappedData()) + reinterpret_cast<SizeT>(data);
    }

    void TexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
                             GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureInputFormat textureInputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
        TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);

        if (!TextureImpl::ValidateTexturePixelDataType(texturePixelDataType)) return;
        if (!TextureImpl::ValidateTextureInputFormat(textureInputFormat)) return;
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
        if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;

        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        auto& textureObject = bindingSlot.GetBoundObject();
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!TextureImpl::ValidateTextureSubImageOffsets(textureObject, xoffset, width, yoffset, height, zoffset,
                                                         depth))
            return;
        if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureObject->GetFormat(),
                                                                           texturePixelDataType))
            return;
        if (!ValidatePixelUnpackBufferSource(pixels, textureInputFormat, texturePixelDataType, {width, height, depth},
                                             __func__))
            return;

        MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
                        "Texture object here should always be an object with mipmap");
        auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        if (static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture level is out of range."));
            return;
        }

        const void* originalPixels = pixels;
        const auto& pixelUnpackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (pixelUnpackBufferObject) {
            originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
                             reinterpret_cast<SizeT>(pixels);
        }
        if (!originalPixels) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "No data supplied from pixels parameter and no PBO bound."));
            return;
        }

        SizeT inputSize = 0;
        void* processedPixels = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
            originalPixels, MG_State::pGLContext->GetPixelStoreParameters(true), textureObject->GetFormat(),
            textureInputFormat, texturePixelDataType, {width, height, depth}, false, inputSize);
        if (!processedPixels || inputSize == 0) {
            if (processedPixels) free(processedPixels);
            return;
        }

        const auto texelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level);
        const SizeT internalBpp = MG_Util::GetInternalBytesPerPixel(textureObject->GetFormat(), texturePixelDataType);
        const SizeT srcRowSize = static_cast<SizeT>(width) * internalBpp;
        const SizeT srcSliceSize = static_cast<SizeT>(height) * srcRowSize;
        const SizeT destRowSize = static_cast<SizeT>(texelSize.x()) * internalBpp;
        const SizeT destSliceSize = static_cast<SizeT>(texelSize.y()) * destRowSize;

        if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
            yoffset + height > static_cast<GLsizei>(texelSize.y()) ||
            zoffset + depth > static_cast<GLsizei>(texelSize.z())) {
            MGLOG_E_ONCE("TexSubImage3D_State: Specified region exceeds texture level dimensions");
            free(processedPixels);
            return;
        }

        const auto* srcData = static_cast<const Uint8*>(processedPixels);
        Uint8* destData = static_cast<Uint8*>(textureMipmapObject->MapMipmapData(textureUploadTarget, level));
        if (destData) {
            for (GLsizei z = 0; z < depth; ++z) {
                for (GLsizei y = 0; y < height; ++y) {
                    const SizeT destRowOffset =
                        static_cast<SizeT>(zoffset + z) * destSliceSize +
                        static_cast<SizeT>(yoffset + y) * destRowSize +
                        static_cast<SizeT>(xoffset) * internalBpp;
                    const SizeT srcRowOffset =
                        static_cast<SizeT>(z) * srcSliceSize + static_cast<SizeT>(y) * srcRowSize;
                    Memcpy(destData + destRowOffset, srcData + srcRowOffset, srcRowSize);
                }
            }
        }

        free(processedPixels);
        textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, yoffset, zoffset},
                                                    {width, height, depth});
        MaybeAutoGenerateMipmap(target, textureObject, false, level);
    }

    void TexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height,
                             GLenum format, GLenum type, const void* pixels) {
        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureInputFormat textureInputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
        TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
        //            TextureInternalFormat textureInternalFormat =
        //            MG_Util::ConvertGLEnumToTextureInternalFormat(format);
        MGLOG_D("TexSubImage2D_State: target = %s, level = %d, (%d, %d), format = %s, pixels = %p",
                MG_Util::ConvertGLEnumToString(target).c_str(), level, width, height,
                MG_Util::ConvertTextureInputFormatToString(textureInputFormat).c_str(), pixels);
        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTexturePixelDataType(texturePixelDataType)) return;
        if (!TextureImpl::ValidateTextureInputFormat(textureInputFormat)) return;
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
        if (!TextureImpl::ValidateTextureSizeRange(width, height, 1)) return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;

        if (!ValidatePixelUnpackBufferSource(pixels, textureInputFormat, texturePixelDataType, {width, height, 1},
                                             __func__))
            return;

        // ======================= Processing ================================
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        auto& textureObject = bindingSlot.GetBoundObject();
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        TextureInternalFormat textureInternalFormat = textureObject->GetFormat();
        MGLOG_D("%s: working on texture %d", __func__, textureObject->GetExternalIndex());

        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTextureSubImageOffsets(textureObject, xoffset, width, yoffset, height)) return;
        if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureInternalFormat,
                                                                           texturePixelDataType))
            return;

        // ======================= Processing ================================
        // Texture object here should always be an object with mipmap
        // Assert this for extra safety.
        // This should automatically compiled out in release,
        // so that we don't take the perf hit of dyn-cast.
        MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
                        "Texture object here should always be an object with mipmap");
        auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        if (static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture level is out of range."));
            return;
        }
        auto texelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level);

        SizeT inputSize = 0;

        const void* originalPixels = pixels;

        // PBO
        const auto& pixelUnpackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (pixelUnpackBufferObject) {
            MGLOG_D("TexSubImage2D_State: Using Pixel Unpack Buffer Object ID: %u",
                    pixelUnpackBufferObject->GetExternalIndex());
            originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
                             reinterpret_cast<SizeT>(pixels);
        }

        if (!originalPixels) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "No data supplied from pixels parameter and no PBO bound."));
            return;
        }
        const auto& unpackParams = MG_State::pGLContext->GetPixelStoreParameters(true);
        void* processedPixels = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
            originalPixels, unpackParams, textureInternalFormat, textureInputFormat, texturePixelDataType,
            {width, height, 1}, false, inputSize);

        if (!processedPixels || inputSize == 0) {
            MGLOG_E_ONCE("TexSubImage2D_State: Failed to process pixel data for TexSubImage2D, width: %d, height: %d", width,
                    height);
            if (processedPixels) free(processedPixels);
            return;
        }

        const SizeT internalBpp = MG_Util::GetInternalBytesPerPixel(textureInternalFormat, texturePixelDataType);

        const SizeT srcRowSize = static_cast<SizeT>(width) * internalBpp;
        const SizeT srcStride = srcRowSize;
        const SizeT destRowSize = static_cast<SizeT>(texelSize.x()) * internalBpp;

        if (xoffset + width > static_cast<GLsizei>(texelSize.x()) ||
            yoffset + height > static_cast<GLsizei>(texelSize.y())) {
            MGLOG_E_ONCE("TexSubImage2D_State: Specified region exceeds texture dimensions");
            free(processedPixels);
            return;
        }

        const auto* srcData = static_cast<const Uint8*>(processedPixels);
        Uint8* destData = static_cast<Uint8*>(textureMipmapObject->MapMipmapData(textureUploadTarget, level));

        if (destData) {
            for (GLsizei y = 0; y < height; y++) {
                const SizeT destRowOffset = (yoffset + y) * destRowSize + xoffset * internalBpp;
                const SizeT srcRowOffset = y * srcStride;
                Memcpy(destData + destRowOffset, srcData + srcRowOffset, srcRowSize);
            }
        }

        free(processedPixels);

        MGLOG_D("%s: mark mip %d as dirty", __func__, level);
        textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, yoffset, 0},
                                                    {width, height, 1});
        MaybeAutoGenerateMipmap(target, textureObject, false, level);
    }

    void TexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type,
                             const GLvoid* pixels) {
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureInputFormat textureInputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
        TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);

        if (!TextureImpl::ValidateTexturePixelDataType(texturePixelDataType)) return;
        if (!TextureImpl::ValidateTextureInputFormat(textureInputFormat)) return;
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, 1)) return;
        if (!TextureImpl::ValidateTextureSizeRange(width, 1, 1)) return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;

        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        auto& textureObject = bindingSlot.GetBoundObject();
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!TextureImpl::ValidateTextureSubImageOffsets(textureObject, xoffset, width)) return;
        if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureObject->GetFormat(),
                                                                           texturePixelDataType))
            return;
        if (!ValidatePixelUnpackBufferSource(pixels, textureInputFormat, texturePixelDataType, {width, 1, 1}, __func__))
            return;

        MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
                        "Texture object here should always be an object with mipmap");
        auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());

        const void* originalPixels = pixels;
        const auto& pixelUnpackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (pixelUnpackBufferObject) {
            originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
                             reinterpret_cast<SizeT>(pixels);
        }
        if (!originalPixels) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "No data supplied from pixels parameter and no PBO bound."));
            return;
        }

        SizeT inputSize = 0;
        void* processedPixels = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
            originalPixels, MG_State::pGLContext->GetPixelStoreParameters(true), textureObject->GetFormat(),
            textureInputFormat, texturePixelDataType, {width, 1, 1}, false, inputSize);
        if (!processedPixels || inputSize == 0) {
            if (processedPixels) free(processedPixels);
            return;
        }

        const SizeT internalBpp = MG_Util::GetInternalBytesPerPixel(textureObject->GetFormat(), texturePixelDataType);
        const SizeT copySize = static_cast<SizeT>(width) * internalBpp;
        Uint8* destData = static_cast<Uint8*>(textureMipmapObject->MapMipmapData(textureUploadTarget, level));
        if (destData) {
            Memcpy(destData + static_cast<SizeT>(xoffset) * internalBpp, processedPixels, copySize);
        }

        free(processedPixels);
        textureMipmapObject->MarkStorageDirtyRegion(textureUploadTarget, level, {xoffset, 0, 0}, {width, 1, 1});
        MaybeAutoGenerateMipmap(target, textureObject, false, level);
    }

    // TexParameteriv/TexParameterfv are introduced in OpenGL 4.0, so do not support them for now.
    void TexParameterf_State(GLenum target, GLenum pname, GLfloat param) {
        if (!ValidateTextureParameterTarget(target, __func__)) return;

        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

        // ======================= Processing ================================
        auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
        if (!textureObject) return;

        // The per-object setters run this before writing, and glTexParameterfv/glTextureParameterfv
        // funnel everything that is not a vector pname down here - so without it the float forms of
        // the setter accepted sampler state on a multisample texture, a mipmapping filter on a
        // rectangle texture and a negative base level, all of which the integer forms rejected.
        const GLint validationParam = pname == GL_TEXTURE_MAX_ANISOTROPY_EXT ? 1 : static_cast<GLint>(param);
        if (!ValidateTextureParameterForTarget(textureObject, pname, validationParam, __func__)) return;

        switch (pname) {
        case GL_TEXTURE_MAG_FILTER:
            textureObject->GetSamplerObject()->SetMagFilter(MG_Util::ConvertGLEnumToSamplerFilterMode((GLenum)param));
            break;
        case GL_TEXTURE_MIN_FILTER:
            textureObject->GetSamplerObject()->SetMinFilter(MG_Util::ConvertGLEnumToSamplerFilterMode((GLenum)param));
            textureObject->GetSamplerObject()->SetMipmapMode(MG_Util::ConvertGLEnumToSamplerMipmapMode((GLenum)param));
            break;
        case GL_TEXTURE_MIN_LOD: {
            Float maxLod = textureObject->GetSamplerObject()->GetMaxLod();
            textureObject->GetSamplerObject()->SetLodRange(param, maxLod);
            break;
        }
        case GL_TEXTURE_MAX_LOD: {
            Float minLod = textureObject->GetSamplerObject()->GetMinLod();
            textureObject->GetSamplerObject()->SetLodRange(minLod, param);
            break;
        }
        case GL_TEXTURE_BASE_LEVEL:
            textureObject->SetBaseLevel((Uint)param);
            break;
        case GL_TEXTURE_MAX_LEVEL:
            textureObject->SetMaxLevel((Uint)param);
            break;
        case GL_TEXTURE_SWIZZLE_R:
        case GL_TEXTURE_SWIZZLE_G:
        case GL_TEXTURE_SWIZZLE_B:
        case GL_TEXTURE_SWIZZLE_A: {
            auto swizzleParam = MG_Util::ConvertGLEnumPnameToTextureSwizzleParam(pname);
            auto swizzleValue = MG_Util::ConvertGLEnumToTextureSwizzleParam((GLenum)param);
            if (swizzleValue == TextureSwizzleParam::Unknown) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Invalid texture swizzle value."));
                return;
            }
            textureObject->SetSwizzleParam(swizzleParam, swizzleValue);
            break;
        }
        case GL_TEXTURE_WRAP_S:
            textureObject->GetSamplerObject()->SetWrapS(MG_Util::ConvertGLEnumToSamplerWrapMode((GLenum)param));
            break;
        case GL_TEXTURE_WRAP_T:
            textureObject->GetSamplerObject()->SetWrapT(MG_Util::ConvertGLEnumToSamplerWrapMode((GLenum)param));
            break;
        case GL_TEXTURE_WRAP_R:
            textureObject->GetSamplerObject()->SetWrapR(MG_Util::ConvertGLEnumToSamplerWrapMode((GLenum)param));
            break;
        case GL_TEXTURE_COMPARE_MODE:
            textureObject->GetSamplerObject()->SetCompareMode(
                MG_Util::ConvertGLEnumToSamplerCompareMode((GLenum)param));
            break;
        case GL_TEXTURE_COMPARE_FUNC:
            textureObject->GetSamplerObject()->SetSamplerCompareFunc(
                MG_Util::ConvertGLEnumToSamplerCompareFunc((GLenum)param));
            break;
        case GL_TEXTURE_LOD_BIAS:
            textureObject->GetSamplerObject()->SetLodBias(param);
            break;
        case GL_TEXTURE_MAX_ANISOTROPY_EXT:
            if (!ValidateMaxAnisotropy(param, __func__)) return;
            textureObject->GetSamplerObject()->SetMaxAnisotropy(param);
            break;
        case GL_GENERATE_MIPMAP:
            g_autoGenerateMipmapByTextureId[textureObject->GetExternalIndex()] = (param != 0.0f);
            break;
        case GL_TEXTURE_SWIZZLE_RGBA:
            // Not supported in this function
        case GL_TEXTURE_BORDER_COLOR:
            // Not supported in this function
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", __func__,
                    std::format("pname {} is not a valid texture parameter.", MG_Util::ConvertGLEnumToString(pname))));
            return;
        }
    }

    void TexParameteri_State(GLenum target, GLenum pname, GLint param) {
        if (!ValidateTextureParameterTarget(target, __func__)) return;

        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

        // ======================= Processing ================================
        auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
        if (!textureObject) return;

        TextureParameterObject_State(textureObject, pname, param, __func__);
    }

    // Quick and dirty TexParameter*v implementation to make NeoForge happy.
    // TODO: implement the missing part
    void TexParameterfv_State(GLenum target, GLenum pname, const GLfloat* params) {
        if (!ValidateTextureParameterTarget(target, __func__)) return;
        switch (pname) {
        case GL_TEXTURE_BORDER_COLOR: {
            // ======================= Converting ================================
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

            // ======================= Processing ================================
            auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
            if (!textureObject) return;
            // The vector setters reach the border colour without passing through the per-object
            // validator the scalar ones use, so the multisample gate has to be asked for explicitly -
            // otherwise glTexParameterfv(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_BORDER_COLOR, ...)
            // is accepted while the scalar spelling of the same call is not.
            if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
            SetTextureBorderColorFromFloats(textureObject, params);
            break;
        }
        case GL_TEXTURE_SWIZZLE_RGBA: {
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
            auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
            if (!textureObject) return;
            GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
                                     static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
            if (!SetTextureSwizzleParamsFromInts(textureObject, signedParams, __func__)) {
                return;
            }
            break;
        }
        default:
            TexParameterf_State(target, pname, *params);
            break;
        }
    }

    void TexParameteriv_State(GLenum target, GLenum pname, const GLint* params) {
        if (!ValidateTextureParameterTarget(target, __func__)) return;
        switch (pname) {
        case GL_TEXTURE_BORDER_COLOR: {
            // ======================= Converting ================================
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

            // ======================= Processing ================================
            auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
            if (!textureObject) return;
            if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
            SetTextureBorderColorFromInts(textureObject, params);
            break;
        }
        case GL_TEXTURE_SWIZZLE_RGBA: {
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
            auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
            if (!textureObject) return;
            if (!SetTextureSwizzleParamsFromInts(textureObject, params, __func__)) {
                return;
            }
            break;
        }
        default:
            TexParameteri_State(target, pname, *params);
            break;
        }
    }

    void TexParameterIiv_State(GLenum target, GLenum pname, const GLint* params) {
        if (!ValidateTextureParameterTarget(target, __func__)) return;
        switch (pname) {
        case GL_TEXTURE_BORDER_COLOR: {
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
            auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
            if (!textureObject) return;
            if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
            SetTextureBorderColorFromIntegerInts(textureObject, params);
            break;
        }
        case GL_TEXTURE_SWIZZLE_RGBA: {
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
            auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
            if (!textureObject) return;
            GLint signedParams[4] = {static_cast<GLint>(params[0]), static_cast<GLint>(params[1]),
                                     static_cast<GLint>(params[2]), static_cast<GLint>(params[3])};
            if (!SetTextureSwizzleParamsFromInts(textureObject, signedParams, __func__)) {
                return;
            }
            break;
        }
        default:
            TexParameteri_State(target, pname, *params);
            break;
        }
    }

    void TexParameterIuiv_State(GLenum target, GLenum pname, const GLuint* params) {
        if (!ValidateTextureParameterTarget(target, __func__)) return;
        switch (pname) {
        case GL_TEXTURE_BORDER_COLOR: {
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
            auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
            if (!textureObject) return;
            if (!ValidateTextureParameterForTarget(textureObject, GL_TEXTURE_BORDER_COLOR, 0, __func__)) return;
            SetTextureBorderColorFromUnsignedInts(textureObject, params);
            break;
        }
        case GL_TEXTURE_SWIZZLE_RGBA: {
            // ======================= Converting ================================
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

            // ======================= Processing ================================
            auto& textureObject = GetTextureObjectByTargetForParameter(textureUploadTarget, textureTarget);
            if (!textureObject) return;

            Vec4<TextureSwizzleParam> swizzleParams;
            for (int i = 0; i < 4; i++) {
                swizzleParams[i] = MG_Util::ConvertGLEnumToTextureSwizzleParam(static_cast<GLint>(params[i]));
                if (TextureSwizzleParam::Unknown == swizzleParams[i]) {
                    MG_State::pGLContext->RecordError(
                        ErrorCode::InvalidEnum,
                        MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "`params` is not valid."));
                    return;
                }
            }
            textureObject->SetSwizzleParamRGBA(swizzleParams);
            break;
        }
        default:
            TexParameteri_State(target, pname, static_cast<GLint>(*params));
            break;
        }
    }

    Bool TexImage3DMultisample_State(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
                                     GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) {
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);

        if (!TextureImpl::ValidateTextureTarget(textureTarget)) return false;
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return false;
        if (textureTarget != TextureTarget::Texture2DMultisampleArray) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Target must be GL_TEXTURE_2D_MULTISAMPLE_ARRAY or its proxy."));
            return false;
        }

        textureInternalFormat = MG_Util::ConvertInternalFormatToSized(textureInternalFormat, TextureInputFormat::RGBA,
                                                                      TexturePixelDataType::UnsignedByte);
        if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return false;
        if (!ValidateTextureMultisampleStorage(textureTarget, samples, width, height, depth, textureInternalFormat,
                                               __func__))
            return false;

        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        const Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();
        // Name 0 resolves to the target's default texture object - a real texture this call
        // (re)specifies like any other; the slot is never empty anymore.
        if (!TextureImpl::ValidateTextureObject(textureObject)) return false;
        if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
            return false;
        }

        AllocateMultisampleTextureStorage(textureObject, textureUploadTarget, textureInternalFormat, samples, width,
                                          height, depth, fixedsamplelocations);
        return true;
    }

    Bool TexImage2DMultisample_State(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
                                     GLsizei height, GLboolean fixedsamplelocations) {
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);

        if (!TextureImpl::ValidateTextureTarget(textureTarget)) return false;
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return false;
        if (textureTarget != TextureTarget::Texture2DMultisample) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Target must be GL_TEXTURE_2D_MULTISAMPLE or its proxy."));
            return false;
        }

        textureInternalFormat = MG_Util::ConvertInternalFormatToSized(textureInternalFormat, TextureInputFormat::RGBA,
                                                                      TexturePixelDataType::UnsignedByte);
        if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return false;
        if (!ValidateTextureMultisampleStorage(textureTarget, samples, width, height, 1, textureInternalFormat,
                                               __func__))
            return false;

        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        const Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();
        // Name 0 resolves to the target's default texture object - a real texture this call
        // (re)specifies like any other; the slot is never empty anymore.
        if (!TextureImpl::ValidateTextureObject(textureObject)) return false;
        if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
            return false;
        }

        AllocateMultisampleTextureStorage(textureObject, textureUploadTarget, textureInternalFormat, samples, width,
                                          height, 1, fixedsamplelocations);
        return true;
    }

    void TexImage3D_State(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height,
                          GLsizei depth, GLint border, GLenum format, GLenum type, const void* pixels) {
        MGLOG_D(
            "%s called with target: %s, level: %d, internalformat: %s, width: %d, height: %d, depth: %d, "
            "border: %d, format: %s, type: %s (%u), pixels: %p",
            __func__,
            MG_Util::ConvertTextureUploadTargetToString(MG_Util::ConvertGLEnumToTextureUploadTarget(target)).c_str(),
            level,
            MG_Util::ConvertTextureInternalFormatToString(MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat))
                .c_str(),
            width, height, depth, border,
            MG_Util::ConvertTextureInputFormatToString(MG_Util::ConvertGLEnumToTextureInputFormat(format)).c_str(),
            MG_Util::ConvertTexturePixelDataTypeToString(MG_Util::ConvertGLEnumToTexturePixelDataType(type)).c_str(),
            type, pixels);
        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureInputFormat textureInputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
        TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);

        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTexturePixelDataType(texturePixelDataType)) return;
        if (!TextureImpl::ValidateTextureInputFormat(textureInputFormat)) return;
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
        if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return;
        if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return;
        if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
        if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
        if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureInternalFormat,
                                                                           texturePixelDataType))
            return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;

        // Depth and depth-stencil formats are not three-dimensional in core GL (2D-array targets are fine).
        if ((textureUploadTarget == TextureUploadTarget::Texture3D ||
             textureUploadTarget == TextureUploadTarget::ProxyTexture3D) &&
            (textureInputFormat == TextureInputFormat::DepthComponent ||
             textureInputFormat == TextureInputFormat::DepthStencil)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Depth formats are invalid for 3D texture targets"));
            return;
        }

        // RGTC is a 2D-only compression scheme, so a 3D target rejects it. This has to be tested on
        // the raw enum: the RGTC formats resolve to plain R8/RG8/SNORM storage on the way in (see
        // GLToMG's TextureEnumConverter), so once the internal format is converted there is nothing
        // left to distinguish them from an ordinary one- or two-channel upload.
        if ((textureUploadTarget == TextureUploadTarget::Texture3D ||
             textureUploadTarget == TextureUploadTarget::ProxyTexture3D) &&
            (internalformat == GL_COMPRESSED_RED_RGTC1 || internalformat == GL_COMPRESSED_SIGNED_RED_RGTC1 ||
             internalformat == GL_COMPRESSED_RG_RGTC2 || internalformat == GL_COMPRESSED_SIGNED_RG_RGTC2)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "RGTC compressed formats are invalid for 3D texture targets"));
            return;
        }

        // TODO: GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the
        // GL_PIXEL_UNPACK_BUFFER target and the buffer object's data store is currently mapped.
        // GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER
        // target and the data would be unpacked from the buffer object such that the memory reads required would
        // exceed the data store size.
        // GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER
        // target and data is not evenly divisible into the number of bytes needed to store in memory a datum
        // indicated by type.
        // ======================= Processing ================================
        textureInternalFormat =
            MG_Util::ConvertInternalFormatToSized(textureInternalFormat, textureInputFormat, texturePixelDataType);
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();

        // ===================== Error Checking ==============================
        // Name 0 resolves to the target's default texture object - a real texture this call
        // (re)specifies like any other; the slot is never empty anymore.
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!ValidateTextureMutable(textureObject, __func__)) return;

        // ======================= Processing ================================
        if (internalformat == GL_ALPHA || format == GL_ALPHA) {
            textureObject->SetSwizzleParamRGBA({TextureSwizzleParam::Zero, TextureSwizzleParam::Zero,
                                                TextureSwizzleParam::Zero, TextureSwizzleParam::Red});
        }

        SizeT imageSize = 0;
        const SizeT inputBpp = MG_Util::GetInputBytesPerPixel(textureInputFormat, texturePixelDataType);
        const SizeT internalBpp = MG_Util::GetInternalBytesPerPixel(textureInternalFormat, texturePixelDataType);
        const SizeT internalBytes = width * height * depth * internalBpp;

        textureObject->SetInternalFormat(textureInternalFormat);

        const void* originalPixels = pixels;

        // PBO
        const auto& pixelUnpackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (pixelUnpackBufferObject) {
            MGLOG_D("%s: Using Pixel Unpack Buffer Object ID: %u", __func__,
                    pixelUnpackBufferObject->GetExternalIndex());
            originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
                             reinterpret_cast<SizeT>(pixels);
        }

        MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
                        "Texture object here should always be an object with mipmap");
        auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());

        // Allocate in TextureObject
        if (isProxy) {
            MGLOG_D("%s: isProxy = true, not allocating", __func__);
        } else {
            DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
            textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
            // The same specific-compressed-format tag glTexImage2D records (see TexImage2D_State):
            // GL 4.6 core 8.5 commits the level to that format, so GL_TEXTURE_COMPRESSED and
            // GL_TEXTURE_INTERNAL_FORMAT must report it - and, less obviously, glCopyImageSubData
            // sizes the level's texel BLOCK from it. Without the tag a GL_COMPRESSED_RG_RGTC2
            // array level measured as the RG8 storage it resolved to, 2 bytes instead of 16, and
            // the copy-compatibility rule refused a pairing 18.3.2 requires. AllocateStorage above
            // clears the tag, so this has to follow it.
            const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast<GLenum>(internalformat));
            if (compressedInfo.blockWidth != 0) {
                textureMipmapObject->SetMipmapCompressedImage(
                    textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
                    MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth}));
            }
            // Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
            // enums too, which the tag above deliberately skips - glClearTexImage has to refuse
            // them all (GL 4.6 core 8.19).
            if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
                textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
                                                                        static_cast<GLenum>(internalformat));
            }
        }

        if (!originalPixels) {
            MGLOG_D("%s: No input pixel and no PBO bound, no pixel transfer", __func__);
            return;
        }

        void* processedPixels = nullptr;
        processedPixels = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
            originalPixels, MG_State::pGLContext->GetPixelStoreParameters(true), textureInternalFormat,
            textureInputFormat, texturePixelDataType, {width, height, depth}, false, imageSize);

        if (processedPixels && imageSize > 0) {
            if (imageSize != internalBytes) {
                MGLOG_W_ONCE("%s: Processed pixel data size (%zu) does not match expected size (%zu). "
                        "This may indicate an alignment or processing issue.",
                        __func__, imageSize, internalBytes);
            }

            const SizeT copySize = std::min(imageSize, internalBytes);
            DataPtr texelInput{processedPixels, copySize};
            textureMipmapObject->UpdateMipmapSubData(textureUploadTarget, level, texelInput);
        }

        textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);

        free(processedPixels);
        MaybeAutoGenerateMipmap(target, textureObject, isProxy, level);
    }

    void TexImage2D_State(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border,
                          GLenum format, GLenum type, const void* pixels) {
        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureInputFormat textureInputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
        TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        MGLOG_D("%s called with target: %s (%s), level: %d, internalformat: %s (%s), width: %d, height: %d, "
                "border: %d, format: %s (%s), type: %s (%s), pixels: %p",
                __func__, MG_Util::ConvertTextureUploadTargetToString(textureUploadTarget).c_str(),
                MG_Util::ConvertGLEnumToString(target).c_str(), level,
                MG_Util::ConvertTextureInternalFormatToString(textureInternalFormat).c_str(),
                MG_Util::ConvertGLEnumToString(internalformat).c_str(), width, height, border,
                MG_Util::ConvertTextureInputFormatToString(textureInputFormat).c_str(),
                MG_Util::ConvertGLEnumToString(format).c_str(),
                MG_Util::ConvertTexturePixelDataTypeToString(texturePixelDataType).c_str(),
                MG_Util::ConvertGLEnumToString(type).c_str(), pixels);
        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTexturePixelDataType(texturePixelDataType)) return;
        if (!TextureImpl::ValidateTextureInputFormat(textureInputFormat)) return;
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
        if (!TextureImpl::ValidateTextureSizeRange(width, height, 1)) return;
        if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
        if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
        if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureInternalFormat,
                                                                           texturePixelDataType))
            return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;

        // TODO: GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the
        // GL_PIXEL_UNPACK_BUFFER target and the buffer object's data store is currently mapped.
        // GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER
        // target and the data would be unpacked from the buffer object such that the memory reads required would
        // exceed the data store size.
        // GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to the GL_PIXEL_UNPACK_BUFFER
        // target and data is not evenly divisible into the number of bytes needed to store in memory a datum
        // indicated by type.

        // ======================= Processing ================================
        textureInternalFormat =
            MG_Util::ConvertInternalFormatToSized(textureInternalFormat, textureInputFormat, texturePixelDataType);
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();

        // ===================== Error Checking ==============================
        // Name 0 resolves to the target's default texture object - a real texture this call
        // (re)specifies like any other; the slot is never empty anymore.
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!ValidateTextureMutable(textureObject, __func__)) return;

        // ======================= Processing ================================
        if (internalformat == GL_ALPHA || format == GL_ALPHA) {
            textureObject->SetSwizzleParamRGBA({TextureSwizzleParam::Zero, TextureSwizzleParam::Zero,
                                                TextureSwizzleParam::Zero, TextureSwizzleParam::Red});
        }

        SizeT imageSize = 0;
        const SizeT inputBpp = MG_Util::GetInputBytesPerPixel(textureInputFormat, texturePixelDataType);
        const SizeT internalBpp = MG_Util::GetInternalBytesPerPixel(textureInternalFormat, texturePixelDataType);
        const SizeT internalBytes = width * height * internalBpp;

        MGLOG_D("%s: working on texture %d", __func__, textureObject->GetExternalIndex());

        MGLOG_D("%s: texture object had internal format %s, new format %s", __func__,
                MG_Util::ConvertTextureInternalFormatToString(textureObject->GetFormat()).c_str(),
                MG_Util::ConvertTextureInternalFormatToString(textureInternalFormat).c_str());
        textureObject->SetInternalFormat(textureInternalFormat);

        const void* originalPixels = pixels;

        // PBO
        const auto& pixelUnpackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (pixelUnpackBufferObject) {
            MGLOG_D("%s: Using Pixel Unpack Buffer Object ID: %u", __func__,
                    pixelUnpackBufferObject->GetExternalIndex());
            originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
                             reinterpret_cast<SizeT>(pixels);
        }

        MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
                        "Texture object here should always be an object with mipmap");
        auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());

        // Allocate in TextureObject
        if (isProxy) {
            MGLOG_D("%s: isProxy = true, not allocating", __func__);
        } else {
            MGLOG_D("%s: Allocating %d bytes at mip %d", __func__, internalBytes, level);
            DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
            textureMipmapObject->AllocateStorage(textureUploadTarget, level,
                                                 {{width, height, 1}, internalBytes});
            // GL 4.6 core 8.5: a SPECIFIC compressed internalformat (unlike a generic
            // GL_COMPRESSED_* one, where the implementation is free to choose) commits the
            // level to that format - GL_TEXTURE_COMPRESSED must then answer true for it and
            // GL_TEXTURE_INTERNAL_FORMAT must report it, which is how an application asks for
            // the size to hand glCompressedTexSubImage2D afterwards. Only the tag and the size
            // are recorded: there is no BC/ETC codec here, so the texel shadow keeps the
            // uncompressed storage this format resolved to (which is also what lets the level
            // sample as the application's texels), and the compressed image the tag describes
            // is zero-filled - the one reproducible answer glGetCompressedTexImage can give for
            // an image nothing ever compressed. AllocateStorage above clears the tag, so this
            // has to follow it.
            const auto compressedInfo = MG_Util::GetCompressedFormatInfo(static_cast<GLenum>(internalformat));
            if (compressedInfo.blockWidth != 0) {
                textureMipmapObject->SetMipmapCompressedImage(
                    textureUploadTarget, level, static_cast<GLenum>(internalformat), nullptr,
                    MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1}));
            }
            // Also after AllocateStorage, which clears it. Records the generic GL_COMPRESSED_*
            // enums too, which the tag above deliberately skips - glClearTexImage has to refuse
            // them all (GL 4.6 core 8.19).
            if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalformat))) {
                textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
                                                                        static_cast<GLenum>(internalformat));
            }
        }

        if (!originalPixels) {
            MGLOG_D("%s: No input pixel and no PBO bound, no pixel transfer", __func__);
            return;
        }

        void* processedPixels = nullptr;
        processedPixels = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
            originalPixels, MG_State::pGLContext->GetPixelStoreParameters(true), textureInternalFormat,
            textureInputFormat, texturePixelDataType, {width, height, 1}, false, imageSize);

        if (processedPixels && imageSize > 0) {
            if (imageSize != internalBytes) {
                MGLOG_W_ONCE("TexImage2D_State: Processed pixel data size (%zu) does not match expected size (%zu). "
                        "This may indicate an alignment or processing issue.",
                        imageSize, internalBytes);
            }

            const SizeT copySize = std::min(imageSize, internalBytes);
            DataPtr texelInput{processedPixels, copySize};
            textureMipmapObject->UpdateMipmapSubData(textureUploadTarget, level, texelInput);
        }

        free(processedPixels);

        MGLOG_D("%s: mark mip %d as dirty", __func__, level);
        textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
        MaybeAutoGenerateMipmap(target, textureObject, isProxy, level);
    }

    void TexImage1D_State(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLint border, GLenum format,
                          GLenum type, const GLvoid* pixels) {
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureInputFormat textureInputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
        TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalFormat);

        if (!TextureImpl::ValidateTexturePixelDataType(texturePixelDataType)) return;
        if (!TextureImpl::ValidateTextureInputFormat(textureInputFormat)) return;
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, 1)) return;
        if (!TextureImpl::ValidateTextureSizeRange(width, 1, 1)) return;
        if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
        if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
        if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureInternalFormat,
                                                                           texturePixelDataType))
            return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;

        textureInternalFormat =
            MG_Util::ConvertInternalFormatToSized(textureInternalFormat, textureInputFormat, texturePixelDataType);
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();
        // Name 0 resolves to the target's default texture object - a real texture this call
        // (re)specifies like any other; the slot is never empty anymore.
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!ValidateTextureMutable(textureObject, __func__)) return;

        if (internalFormat == GL_ALPHA || format == GL_ALPHA) {
            textureObject->SetSwizzleParamRGBA({TextureSwizzleParam::Zero, TextureSwizzleParam::Zero,
                                                TextureSwizzleParam::Zero, TextureSwizzleParam::Red});
        }

        const SizeT internalBpp = MG_Util::GetInternalBytesPerPixel(textureInternalFormat, texturePixelDataType);
        const SizeT internalBytes = static_cast<SizeT>(width) * internalBpp;
        textureObject->SetInternalFormat(textureInternalFormat);

        const void* originalPixels = pixels;
        const auto& pixelUnpackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (pixelUnpackBufferObject) {
            originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
                             reinterpret_cast<SizeT>(pixels);
        }

        MOBILEGL_ASSERT(nullptr != static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get()),
                        "Texture object here should always be an object with mipmap");
        auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        if (!isProxy) {
            DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
            textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
            // After AllocateStorage, which clears the tag. No block-compressed format has a 1D
            // layout, so only the specific-format tag the 2D/3D paths record is skipped here - the
            // request itself still has to be remembered for glClearTexImage (GL 4.6 core 8.19).
            if (IsCompressedGLInternalFormat(static_cast<GLenum>(internalFormat))) {
                textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget, level,
                                                                        static_cast<GLenum>(internalFormat));
            }
        }

        if (!originalPixels) {
            return;
        }

        SizeT imageSize = 0;
        void* processedPixels = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
            originalPixels, MG_State::pGLContext->GetPixelStoreParameters(true), textureInternalFormat,
            textureInputFormat, texturePixelDataType, {width, 1, 1}, false, imageSize);
        if (processedPixels && imageSize > 0) {
            DataPtr texelInput{processedPixels, std::min(imageSize, internalBytes)};
            textureMipmapObject->UpdateMipmapSubData(textureUploadTarget, level, texelInput);
        }

        free(processedPixels);
        textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
        MaybeAutoGenerateMipmap(target, textureObject, isProxy, level);
    }

    // The work glTexBuffer[Range] and glTextureBuffer[Range] all share once the texture has been
    // resolved - by binding for the target forms, by name for the DSA ones. `size` is
    // kWholeBuffer for the non-Range entry points, which attach the buffer as it grows rather
    // than freezing the size it happens to have now.
    // The sized internal formats a buffer texture accepts (GL 4.6 core table 8.16). This is a much
    // shorter list than the renderable or texturable formats, so it cannot be inferred from either.
    Bool IsBufferTextureInternalFormat(GLenum internalformat) {
        switch (internalformat) {
        case GL_R8:
        case GL_R16:
        case GL_R16F:
        case GL_R32F:
        case GL_R8I:
        case GL_R16I:
        case GL_R32I:
        case GL_R8UI:
        case GL_R16UI:
        case GL_R32UI:
        case GL_RG8:
        case GL_RG16:
        case GL_RG16F:
        case GL_RG32F:
        case GL_RG8I:
        case GL_RG16I:
        case GL_RG32I:
        case GL_RG8UI:
        case GL_RG16UI:
        case GL_RG32UI:
        case GL_RGB32F:
        case GL_RGB32I:
        case GL_RGB32UI:
        case GL_RGBA8:
        case GL_RGBA16:
        case GL_RGBA16F:
        case GL_RGBA32F:
        case GL_RGBA8I:
        case GL_RGBA16I:
        case GL_RGBA32I:
        case GL_RGBA8UI:
        case GL_RGBA16UI:
        case GL_RGBA32UI:
            return true;
        default:
            return false;
        }
    }

    // GL 4.6 core 8.9 / GL_EXT_texture_buffer: the two TARGET-taking forms (glTexBuffer,
    // glTexBufferRange) accept exactly GL_TEXTURE_BUFFER, and anything else is GL_INVALID_ENUM.
    // Checked up front rather than left to fall out of "the bound object is not a buffer texture"
    // deeper in, because that path's error code depends on which entry point took it - the
    // name-taking DSA forms owe GL_INVALID_OPERATION for the same shape - and because for some
    // targets it did not reach that check at all. esextcTextureBufferErrors walks every other
    // texture target through both entry points and reads the code back each time.
    static Bool ValidateBufferTextureTarget(GLenum target, const char* caller) {
        if (target == GL_TEXTURE_BUFFER) return true;
        MG_State::pGLContext->RecordError(
            ErrorCode::InvalidEnum,
            MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                         std::format("target 0x{:X} is not GL_TEXTURE_BUFFER.", target)));
        return false;
    }

    static void AttachBufferToTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                      GLenum internalformat, GLuint buffer, GLintptr offset, SizeT size,
                                      const char* caller) {
        using MG_State::GLState::TextureObjectBuffer;
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        if (!IsBufferTextureInternalFormat(internalformat)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    std::format("internalformat 0x{:X} is not one of the sized formats a buffer texture accepts.",
                                internalformat)));
            return;
        }
        if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;

        auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
        if (buffer != 0 && !bufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "`buffer` is not zero and is not the name of an existing buffer object."));
            return;
        }
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
            // A texture whose target is something else is a wrong object, not a wrong token
            // (GL 4.6 core 8.9).
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "The effective target of `texture` is not `GL_TEXTURE_BUFFER`."));
            return;
        }
        if (size != TextureObjectBuffer::kWholeBuffer) {
            // GL 4.6 core 8.9: offset must be non-negative and aligned to
            // GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT, and size must be positive.
            if (offset < 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "offset must be non-negative."));
                return;
            }
            if (size == 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "size must be greater than zero."));
                return;
            }
            const Int alignment = std::max(
                1, MG_Backend::pActiveBackendObject->GetDynamicParameters().TextureBufferOffsetAlignment);
            if (offset % alignment != 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "offset is not a multiple of GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT."));
                return;
            }
            // The range has to lie inside the buffer that is being attached. Detaching (buffer
            // zero) carries no range to check.
            if (bufferObject && static_cast<SizeT>(offset) + size > bufferObject->GetSize()) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "offset + size is greater than the buffer object's GL_BUFFER_SIZE."));
                return;
            }
        }

        auto* texBufferObject = static_cast<TextureObjectBuffer*>(textureObject.get());
        texBufferObject->GetBufferBindingSlot().Bind(bufferObject);
        texBufferObject->SetBufferRange(static_cast<SizeT>(offset < 0 ? 0 : offset), size);
        texBufferObject->SetInternalFormat(textureInternalFormat);
    }

    void TexBuffer_State(GLenum target, GLenum internalformat, GLuint buffer) {
        // ======================= Converting ================================
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);

        // ===================== Error Checking ==============================
        if (!ValidateBufferTextureTarget(target, __func__)) return;
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
        // The sized-format table a buffer texture accepts (GL 4.6 core table 8.15). The DSA and
        // range forms have always run this through AttachBufferToTexture; this one carried a TODO
        // instead, so glTexBuffer(GL_TEXTURE_BUFFER, GL_DEPTH_COMPONENT32F, ...) succeeded.
        if (!IsBufferTextureInternalFormat(internalformat)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", __func__,
                    std::format("internalformat 0x{:X} is not one of the sized formats a buffer texture accepts.",
                                internalformat)));
            return;
        }
        // GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
        // nonzero name that is not an existing buffer object is an error. This is reachable on
        // the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER),
        // which the GL CTS state reset detaches with glTexBuffer(..., 0) after every case.
        auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
        if (buffer != 0 && !bufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "`buffer` is not zero and is not the name of an existing buffer object."));
            return;
        }

        // ======================= Processing ================================
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        auto& textureObject = bindingSlot.GetBoundObject();

        // ===================== Error Checking ==============================
        // Name 0 is the default buffer texture - a real object the (de)attach operates on, not a
        // silent no-op; the slot is never empty now that every unit/target holds its default.
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
            // Defensive: the target gate above already rejected every target but GL_TEXTURE_BUFFER,
            // whose binding slot only ever holds buffer textures.
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "The effective target of `texture` is not `GL_TEXTURE_BUFFER`."));
            return;
        }

        // ======================= Processing ================================
        // Now we can rest assured, and down-cast texture object to texture buffer
        auto* texBufferObject = static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
        auto& bufferSlot = texBufferObject->GetBufferBindingSlot();
        bufferSlot.Bind(bufferObject);

        texBufferObject->SetBufferRange(0, MG_State::GLState::TextureObjectBuffer::kWholeBuffer);
        texBufferObject->SetInternalFormat(textureInternalFormat);
    }

    GLboolean IsTexture_State(GLuint texture) {
        // ======================= Processing ================================
        // GL 3.3 core 6.1.4: IsTexture generates no error - an unknown, deleted or merely reserved
        // name is just GL_FALSE. Probing with the recording validator (as every other Is* entry
        // point already avoids doing) would leave a spurious INVALID_VALUE behind.
        return MG_State::pGLContext->ValidateTextureObject(texture) ? GL_TRUE : GL_FALSE;
    }

    void GetTexParameterIuiv_State(GLenum target, GLenum pname, GLuint* params) {
        if (params == nullptr) return;
        if (!ValidateTextureParameterTarget(target, __func__)) return;

        if (pname == GL_TEXTURE_BORDER_COLOR) {
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
            auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
            auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
            Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
            auto& textureObject =
                isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
                        : bindingSlot.GetBoundObject();

            if (!TextureImpl::ValidateTextureObject(textureObject)) return;

            const auto& borderColor = textureObject->GetBorderColorUI();
            params[0] = borderColor.x();
            params[1] = borderColor.y();
            params[2] = borderColor.z();
            params[3] = borderColor.w();
            return;
        }

        GLint signedParams[4] = {0, 0, 0, 0};
        if (!GetTexParameteriv_State(target, pname, signedParams)) return;
        const int componentCount = pname == GL_TEXTURE_BORDER_COLOR || pname == GL_TEXTURE_SWIZZLE_RGBA ? 4 : 1;
        for (int i = 0; i < componentCount; ++i) {
            params[i] = static_cast<GLuint>(signedParams[i]);
        }
    }

    void GetTexParameterIiv_State(GLenum target, GLenum pname, GLint* params) {
        if (params == nullptr) return;
        if (!ValidateTextureParameterTarget(target, __func__)) return;

        if (pname == GL_TEXTURE_BORDER_COLOR) {
            TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
            TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
            auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
            auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
            Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
            auto& textureObject =
                isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
                        : bindingSlot.GetBoundObject();

            if (!TextureImpl::ValidateTextureObject(textureObject)) return;

            const auto& borderColor = textureObject->GetBorderColorI();
            params[0] = borderColor.x();
            params[1] = borderColor.y();
            params[2] = borderColor.z();
            params[3] = borderColor.w();
            return;
        }

        GetTexParameteriv_State(target, pname, params);
    }

    Bool GetTexParameteriv_State(GLenum target, GLenum pname, GLint* params) {
        if (!ValidateTextureParameterTarget(target, __func__)) return false;

        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

        // ======================= Processing ================================
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();

        if (!TextureImpl::ValidateTextureObject(textureObject)) return false;

        switch (pname) {
        case GL_TEXTURE_MAG_FILTER:
            if (params) {
                *params = (GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(
                    textureObject->GetSamplerObject()->GetMagFilter(), SamplerMipmapMode::None);
            }
            break;
        case GL_TEXTURE_MIN_FILTER:
            if (params) {
                *params = (GLint)MG_Util::ConvertSamplerFilterModeToGLEnum(
                    textureObject->GetSamplerObject()->GetMinFilter(),
                    textureObject->GetSamplerObject()->GetMipmapMode());
            }
            break;
        case GL_TEXTURE_MIN_LOD:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMinLod());
            }
            break;
        case GL_TEXTURE_MAX_LOD:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMaxLod());
            }
            break;
        case GL_TEXTURE_BASE_LEVEL:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetLevelRange().x());
            }
            break;
        case GL_TEXTURE_MAX_LEVEL:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetLevelRange().y());
            }
            break;
        case GL_TEXTURE_IMMUTABLE_FORMAT:
            if (params) {
                *params = textureObject->IsImmutable() ? GL_TRUE : GL_FALSE;
            }
            break;
        case GL_TEXTURE_IMMUTABLE_LEVELS:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetImmutableLevels());
            }
            break;
        // GL 4.6 core table 23.17. Zero on a mutable texture; TexStorage* seeds the full extent
        // and glTextureView composes onto it (SeedImmutableViewState / TextureView).
        case GL_TEXTURE_VIEW_MIN_LEVEL:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetViewMinLevel());
            }
            break;
        case GL_TEXTURE_VIEW_NUM_LEVELS:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetViewNumLevels());
            }
            break;
        case GL_TEXTURE_VIEW_MIN_LAYER:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetViewMinLayer());
            }
            break;
        case GL_TEXTURE_VIEW_NUM_LAYERS:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetViewNumLayers());
            }
            break;
        case GL_TEXTURE_BORDER_COLOR:
            if (params) {
                // glGetTexParameteriv is the exact inverse of glTexParameteriv: GL 4.6 core
                // equation 2.3 against equation 2.2 on the write side (SetTextureBorderColorFromInts).
                // A bare truncating cast turned the ~4.7e-10 that equation 2.2 makes of a small
                // integer back into 0, so the legal {0,1,2,4} round trip answered {0,0,0,0}. The raw
                // integer border colour is what glGetTexParameterIiv returns, not this.
                const auto& borderColor = textureObject->GetBorderColor();
                params[0] = MG_Util::FloatToSignedNormalizedInt32(borderColor.x());
                params[1] = MG_Util::FloatToSignedNormalizedInt32(borderColor.y());
                params[2] = MG_Util::FloatToSignedNormalizedInt32(borderColor.z());
                params[3] = MG_Util::FloatToSignedNormalizedInt32(borderColor.w());
            }
            break;
        case GL_TEXTURE_SWIZZLE_RGBA:
            if (params) {
                const auto& swizzleParams = textureObject->GetAllSwizzleParams();
                params[0] = static_cast<GLint>(MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams[0]));
                params[1] = static_cast<GLint>(MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams[1]));
                params[2] = static_cast<GLint>(MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams[2]));
                params[3] = static_cast<GLint>(MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams[3]));
            }
            break;
        case GL_TEXTURE_SWIZZLE_R:
            if (params) {
                *params = static_cast<GLint>(
                    MG_Util::ConvertTextureSwizzleParamToGLEnum(textureObject->GetSwizzleParam(TextureSwizzleParam::Red)));
            }
            break;
        case GL_TEXTURE_SWIZZLE_G:
            if (params) {
                *params = static_cast<GLint>(MG_Util::ConvertTextureSwizzleParamToGLEnum(
                    textureObject->GetSwizzleParam(TextureSwizzleParam::Green)));
            }
            break;
        case GL_TEXTURE_SWIZZLE_B:
            if (params) {
                *params = static_cast<GLint>(
                    MG_Util::ConvertTextureSwizzleParamToGLEnum(textureObject->GetSwizzleParam(TextureSwizzleParam::Blue)));
            }
            break;
        case GL_TEXTURE_SWIZZLE_A:
            if (params) {
                *params = static_cast<GLint>(MG_Util::ConvertTextureSwizzleParamToGLEnum(
                    textureObject->GetSwizzleParam(TextureSwizzleParam::Alpha)));
            }
            break;
        case GL_TEXTURE_WRAP_S:
            if (params) {
                *params = (GLint)MG_Util::ConvertSamplerWrapModeToGLEnum(textureObject->GetSamplerObject()->GetWrapS());
            }
            break;
        case GL_TEXTURE_WRAP_T:
            if (params) {
                *params = (GLint)MG_Util::ConvertSamplerWrapModeToGLEnum(textureObject->GetSamplerObject()->GetWrapT());
            }
            break;
        case GL_TEXTURE_WRAP_R:
            if (params) {
                *params = (GLint)MG_Util::ConvertSamplerWrapModeToGLEnum(textureObject->GetSamplerObject()->GetWrapR());
            }
            break;
        case GL_TEXTURE_COMPARE_MODE:
            if (params) {
                *params = (GLint)MG_Util::ConvertSamplerCompareModeToGLEnum(
                    textureObject->GetSamplerObject()->GetCompareMode());
            }
            break;
        case GL_TEXTURE_COMPARE_FUNC:
            if (params) {
                *params = (GLint)MG_Util::ConvertSamplerCompareFuncToGLEnum(
                    textureObject->GetSamplerObject()->GetSamplerCompareFunc());
            }
            break;
        case GL_TEXTURE_MAX_ANISOTROPY_EXT:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetSamplerObject()->GetMaxAnisotropy());
            }
            break;
        case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
            if (params) {
                *params = GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE;
            }
            break;
        case GL_DEPTH_STENCIL_TEXTURE_MODE:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetDepthStencilTextureMode());
            }
            break;
        case GL_TEXTURE_LOD_BIAS:
            if (params) {
                *params = static_cast<GLint>(textureObject->GetSamplerObject()->GetLodBias());
            }
            break;
        default:
            MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexParameteriv_State",
                                                                           "pname is not a valid texture parameter."));
            return false;
        }

        return true;
    }

    void GetTexParameterfv_State(GLenum target, GLenum pname, GLfloat* params) {
        if (!ValidateTextureParameterTarget(target, __func__)) return;

        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

        // ======================= Processing ================================
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();

        if (!TextureImpl::ValidateTextureObject(textureObject)) return;

        switch (pname) {
        case GL_TEXTURE_MAG_FILTER:
            if (params) {
                *params = (GLfloat)MG_Util::ConvertSamplerFilterModeToGLEnum(
                    textureObject->GetSamplerObject()->GetMagFilter(), SamplerMipmapMode::None);
            }
            break;
        case GL_TEXTURE_MIN_FILTER:
            if (params) {
                *params = (GLfloat)MG_Util::ConvertSamplerFilterModeToGLEnum(
                    textureObject->GetSamplerObject()->GetMinFilter(),
                    textureObject->GetSamplerObject()->GetMipmapMode());
            }
            break;
        case GL_TEXTURE_MIN_LOD:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetSamplerObject()->GetMinLod());
            }
            break;
        case GL_TEXTURE_MAX_LOD:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetSamplerObject()->GetMaxLod());
            }
            break;
        case GL_TEXTURE_BASE_LEVEL:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetLevelRange().x());
            }
            break;
        case GL_TEXTURE_MAX_LEVEL:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetLevelRange().y());
            }
            break;
        case GL_TEXTURE_IMMUTABLE_FORMAT:
            if (params) {
                *params = textureObject->IsImmutable() ? 1.0f : 0.0f;
            }
            break;
        case GL_TEXTURE_IMMUTABLE_LEVELS:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetImmutableLevels());
            }
            break;
        // GL 4.6 core table 23.17; the float form answers the same state as the integer one
        // (KHR-GL43.texture_view.gettexparameter queries both).
        case GL_TEXTURE_VIEW_MIN_LEVEL:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetViewMinLevel());
            }
            break;
        case GL_TEXTURE_VIEW_NUM_LEVELS:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetViewNumLevels());
            }
            break;
        case GL_TEXTURE_VIEW_MIN_LAYER:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetViewMinLayer());
            }
            break;
        case GL_TEXTURE_VIEW_NUM_LAYERS:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetViewNumLayers());
            }
            break;
        case GL_TEXTURE_BORDER_COLOR:
            if (params) {
                const auto& borderColor = textureObject->GetBorderColor();
                params[0] = borderColor.x();
                params[1] = borderColor.y();
                params[2] = borderColor.z();
                params[3] = borderColor.w();
            }
            break;
        case GL_TEXTURE_SWIZZLE_R:
            if (params) {
                *params = static_cast<GLfloat>(
                    MG_Util::ConvertTextureSwizzleParamToGLEnum(textureObject->GetSwizzleParam(TextureSwizzleParam::Red)));
            }
            break;
        case GL_TEXTURE_SWIZZLE_G:
            if (params) {
                *params = static_cast<GLfloat>(MG_Util::ConvertTextureSwizzleParamToGLEnum(
                    textureObject->GetSwizzleParam(TextureSwizzleParam::Green)));
            }
            break;
        case GL_TEXTURE_SWIZZLE_B:
            if (params) {
                *params = static_cast<GLfloat>(MG_Util::ConvertTextureSwizzleParamToGLEnum(
                    textureObject->GetSwizzleParam(TextureSwizzleParam::Blue)));
            }
            break;
        case GL_TEXTURE_SWIZZLE_A:
            if (params) {
                *params = static_cast<GLfloat>(MG_Util::ConvertTextureSwizzleParamToGLEnum(
                    textureObject->GetSwizzleParam(TextureSwizzleParam::Alpha)));
            }
            break;
        case GL_TEXTURE_SWIZZLE_RGBA:
            if (params) {
                const auto& swizzleParams = textureObject->GetAllSwizzleParams();
                params[0] = static_cast<GLfloat>(MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams[0]));
                params[1] = static_cast<GLfloat>(MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams[1]));
                params[2] = static_cast<GLfloat>(MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams[2]));
                params[3] = static_cast<GLfloat>(MG_Util::ConvertTextureSwizzleParamToGLEnum(swizzleParams[3]));
            }
            break;
        case GL_TEXTURE_WRAP_S:
            if (params) {
                *params =
                    (GLfloat)MG_Util::ConvertSamplerWrapModeToGLEnum(textureObject->GetSamplerObject()->GetWrapS());
            }
            break;
        case GL_TEXTURE_WRAP_T:
            if (params) {
                *params =
                    (GLfloat)MG_Util::ConvertSamplerWrapModeToGLEnum(textureObject->GetSamplerObject()->GetWrapT());
            }
            break;
        case GL_TEXTURE_WRAP_R:
            if (params) {
                *params =
                    (GLfloat)MG_Util::ConvertSamplerWrapModeToGLEnum(textureObject->GetSamplerObject()->GetWrapR());
            }
            break;
        case GL_TEXTURE_COMPARE_MODE:
            if (params) {
                *params = (GLfloat)MG_Util::ConvertSamplerCompareModeToGLEnum(
                    textureObject->GetSamplerObject()->GetCompareMode());
            }
            break;
        case GL_TEXTURE_COMPARE_FUNC:
            if (params) {
                *params = (GLfloat)MG_Util::ConvertSamplerCompareFuncToGLEnum(
                    textureObject->GetSamplerObject()->GetSamplerCompareFunc());
            }
            break;
        case GL_TEXTURE_MAX_ANISOTROPY_EXT:
            if (params) {
                *params = textureObject->GetSamplerObject()->GetMaxAnisotropy();
            }
            break;
        // GL 4.6 core 8.11 lists this among the parameters EVERY GetTexParameter form answers.
        // It was handled by the iv/Iiv/Iuiv getters and missed by this one, so the float query
        // raised GL_INVALID_ENUM and left the caller's float untouched - which is what
        // KHR-GL4x.shader_image_load_store.basic-api-texParam reads back.
        case GL_IMAGE_FORMAT_COMPATIBILITY_TYPE:
            if (params) {
                *params = static_cast<GLfloat>(GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE);
            }
            break;
        case GL_DEPTH_STENCIL_TEXTURE_MODE:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetDepthStencilTextureMode());
            }
            break;
        case GL_TEXTURE_LOD_BIAS:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetSamplerObject()->GetLodBias());
            }
            break;
        default:
            MG_State::pGLContext->RecordError(ErrorCode::InvalidEnum,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexParameterfv_State",
                                                                           "pname is not a valid texture parameter."));
            return;
        }
    }

    void GetTexLevelParameteriv_State(GLenum target, GLint level, GLenum pname, GLint* params) {
        MGLOG_D("GetTexLevelParameteriv_State called");
        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;

        // ======================= Processing ================================
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();

        if (!TextureImpl::ValidateTextureObject(textureObject)) return;

        switch (pname) {
        case GL_TEXTURE_WIDTH:
            if (params) {
                switch (textureObject->GetStorageType()) {
                case TextureStorageType::Mipmap: {
                    const auto textureMipmapObject =
                        static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
                    *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
                    break;
                }
                case TextureStorageType::Buffer:
                    *params = GetBufferTextureTexelWidth(textureObject.get());
                    break;
                default:
                    RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
                    break;
                }
            }
            break;
        case GL_TEXTURE_HEIGHT:
            if (params) {
                switch (textureObject->GetStorageType()) {
                case TextureStorageType::Mipmap: {
                    const auto textureMipmapObject =
                        static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
                    *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
                    break;
                }
                case TextureStorageType::Buffer:
                    *params = 1; // a buffer texture is one-dimensional
                    break;
                default:
                    RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
                    break;
                }
            }
            break;
        case GL_TEXTURE_DEPTH:
            if (params) {
                switch (textureObject->GetStorageType()) {
                case TextureStorageType::Mipmap: {
                    const auto textureMipmapObject =
                        static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
                    *params = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
                    break;
                }
                case TextureStorageType::Buffer:
                    *params = 1; // a buffer texture is one-dimensional
                    break;
                default:
                    RecordUnsupportedLevelQueryStorage("GetTexLevelParameteriv_State", pname);
                    break;
                }
            }
            break;
        case GL_TEXTURE_INTERNAL_FORMAT:
            if (params) {
                // A level stored compressed must report the token it was given, not the
                // uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
                // that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
                // every other level answers with its resolved storage format.
                const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
                *params = (compressedFormat != GL_NONE)
                              ? (GLint)compressedFormat
                              : (GLint)MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
            }
            break;
        case GL_TEXTURE_SAMPLES:
            if (params) {
                *params = textureObject->GetSamples();
            }
            break;
        case GL_TEXTURE_FIXED_SAMPLE_LOCATIONS:
            if (params) {
                *params = textureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE;
            }
            break;
        case GL_TEXTURE_RED_TYPE:
        case GL_TEXTURE_GREEN_TYPE:
        case GL_TEXTURE_BLUE_TYPE:
        case GL_TEXTURE_ALPHA_TYPE:
        case GL_TEXTURE_DEPTH_TYPE:
        case GL_TEXTURE_RED_SIZE:
        case GL_TEXTURE_GREEN_SIZE:
        case GL_TEXTURE_BLUE_SIZE:
        case GL_TEXTURE_ALPHA_SIZE:
        case GL_TEXTURE_DEPTH_SIZE:
        case GL_TEXTURE_STENCIL_SIZE:
        case GL_TEXTURE_SHARED_SIZE:
            if (params) {
                *params = GetTextureLevelComponentParameter(textureObject->GetFormat(), pname);
            }
            break;
        case GL_TEXTURE_COMPRESSED:
            if (params) {
                *params =
                    (GetCompressedLevelFormat(textureObject, textureUploadTarget, level) != GL_NONE) ? GL_TRUE
                                                                                                     : GL_FALSE;
            }
            break;
        case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: {
            // GL 4.6 core 8.11: there is no compressed size to report for an image whose internal
            // format is uncompressed, nor for a proxy target, and the query is INVALID_OPERATION
            // rather than a zero.
            if (isProxy || GetCompressedLevelFormat(textureObject, textureUploadTarget, level) == GL_NONE) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
                        "GL_TEXTURE_COMPRESSED_IMAGE_SIZE needs a compressed, non-proxy texture image."));
                return;
            }
            if (params) {
                const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
                *params = static_cast<GLint>(
                    textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level)));
            }
            break;
        }
        case GL_TEXTURE_BUFFER_SIZE:
        case GL_TEXTURE_BUFFER_OFFSET: {
            // GL 4.6 core 8.9: both describe the window of the attached buffer a GL_TEXTURE_BUFFER
            // texture addresses, so there is nothing to report for any other storage - which is
            // INVALID_OPERATION, the same shape GL_TEXTURE_COMPRESSED_IMAGE_SIZE guards itself with
            // above.
            if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
                        "GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
                return;
            }
            if (params) {
                const auto* bufferTextureObject =
                    static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
                // Basic machine units, and UNCLAMPED - see GetBufferTextureTexelWidth for why this
                // half does not take the GL_MAX_TEXTURE_BUFFER_SIZE clamp that WIDTH does.
                *params = static_cast<GLint>(pname == GL_TEXTURE_BUFFER_SIZE
                                                 ? bufferTextureObject->GetBufferRangeSizeInBytes()
                                                 : bufferTextureObject->GetBufferRangeOffset());
            }
            break;
        }
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameteriv_State",
                                                                     "pname is not a valid texture level parameter."));
            return;
        }
        if (params) MGLOG_D("returned %u", *params);
    }

    void GetTexLevelParameterfv_State(GLenum target, GLint level, GLenum pname, GLfloat* params) {
        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;

        // ======================= Processing ================================
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();

        if (!TextureImpl::ValidateTextureObject(textureObject)) return;

        switch (pname) {
        case GL_TEXTURE_WIDTH:
            if (params) {
                switch (textureObject->GetStorageType()) {
                case TextureStorageType::Mipmap: {
                    const auto textureMipmapObject =
                        static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
                    *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).x();
                    break;
                }
                case TextureStorageType::Buffer:
                    *params = (GLfloat)GetBufferTextureTexelWidth(textureObject.get());
                    break;
                default:
                    RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
                    break;
                }
            }
            break;
        case GL_TEXTURE_HEIGHT:
            if (params) {
                switch (textureObject->GetStorageType()) {
                case TextureStorageType::Mipmap: {
                    const auto textureMipmapObject =
                        static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
                    *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).y();
                    break;
                }
                case TextureStorageType::Buffer:
                    *params = 1.0f; // a buffer texture is one-dimensional
                    break;
                default:
                    RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
                    break;
                }
            }
            break;
        case GL_TEXTURE_DEPTH:
            if (params) {
                switch (textureObject->GetStorageType()) {
                case TextureStorageType::Mipmap: {
                    const auto textureMipmapObject =
                        static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
                    *params = (GLfloat)textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level).z();
                    break;
                }
                case TextureStorageType::Buffer:
                    *params = 1.0f; // a buffer texture is one-dimensional
                    break;
                default:
                    RecordUnsupportedLevelQueryStorage("GetTexLevelParameterfv_State", pname);
                    break;
                }
            }
            break;
        case GL_TEXTURE_INTERNAL_FORMAT:
            if (params) {
                // A level stored compressed must report the token it was given, not the
                // uncompressed format backing it (GL 4.6 core 8.11). glCompressedTexImage2D sets
                // that tag, and so does a glTexImage2D given a SPECIFIC compressed internalformat;
                // every other level answers with its resolved storage format.
                const GLenum compressedFormat = GetCompressedLevelFormat(textureObject, textureUploadTarget, level);
                *params = (GLfloat)((compressedFormat != GL_NONE)
                                        ? compressedFormat
                                        : MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat()));
            }
            break;
        case GL_TEXTURE_SAMPLES:
            if (params) {
                *params = static_cast<GLfloat>(textureObject->GetSamples());
            }
            break;
        case GL_TEXTURE_FIXED_SAMPLE_LOCATIONS:
            if (params) {
                *params = textureObject->HasFixedSampleLocations() ? 1.0f : 0.0f;
            }
            break;
        case GL_TEXTURE_RED_TYPE:
        case GL_TEXTURE_GREEN_TYPE:
        case GL_TEXTURE_BLUE_TYPE:
        case GL_TEXTURE_ALPHA_TYPE:
        case GL_TEXTURE_DEPTH_TYPE:
        case GL_TEXTURE_RED_SIZE:
        case GL_TEXTURE_GREEN_SIZE:
        case GL_TEXTURE_BLUE_SIZE:
        case GL_TEXTURE_ALPHA_SIZE:
        case GL_TEXTURE_DEPTH_SIZE:
        case GL_TEXTURE_STENCIL_SIZE:
        case GL_TEXTURE_SHARED_SIZE:
            if (params) {
                *params = static_cast<GLfloat>(GetTextureLevelComponentParameter(textureObject->GetFormat(), pname));
            }
            break;
        case GL_TEXTURE_COMPRESSED:
            if (params) {
                *params =
                    (GetCompressedLevelFormat(textureObject, textureUploadTarget, level) != GL_NONE) ? 1.0f : 0.0f;
            }
            break;
        case GL_TEXTURE_COMPRESSED_IMAGE_SIZE: {
            // See GetTexLevelParameteriv_State: uncompressed images and proxy targets have no
            // compressed size to report, so GL 4.6 core 8.11 makes the query an error.
            if (isProxy || GetCompressedLevelFormat(textureObject, textureUploadTarget, level) == GL_NONE) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
                        "GL_TEXTURE_COMPRESSED_IMAGE_SIZE needs a compressed, non-proxy texture image."));
                return;
            }
            if (params) {
                const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
                *params = static_cast<GLfloat>(
                    textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level)));
            }
            break;
        }
        case GL_TEXTURE_BUFFER_SIZE:
        case GL_TEXTURE_BUFFER_OFFSET: {
            // See GetTexLevelParameteriv_State: both describe the attached buffer range of a
            // GL_TEXTURE_BUFFER texture, so any other storage makes the query INVALID_OPERATION.
            if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
                        "GL_TEXTURE_BUFFER_SIZE / GL_TEXTURE_BUFFER_OFFSET need a buffer texture."));
                return;
            }
            if (params) {
                const auto* bufferTextureObject =
                    static_cast<MG_State::GLState::TextureObjectBuffer*>(textureObject.get());
                *params = static_cast<GLfloat>(pname == GL_TEXTURE_BUFFER_SIZE
                                                   ? bufferTextureObject->GetBufferRangeSizeInBytes()
                                                   : bufferTextureObject->GetBufferRangeOffset());
            }
            break;
        }
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexLevelParameterfv_State",
                                                                     "pname is not a valid texture level parameter."));
            return;
        }
    }

    // The half glGetCompressedTexImage and glGetCompressedTextureImage share, factored out for the
    // same reason ValidateTextureImageQuery was: the by-name entry point must not drift away from
    // the by-target one's rules. bufSize < 0 means "no destination-size argument" - the by-target
    // form has none (GL 4.6 core 8.11 has the caller size it from GL_TEXTURE_COMPRESSED_IMAGE_SIZE),
    // so only the DSA form passes a real bound.
    void CopyCompressedTextureImageToClientOrPBO(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                                 TextureUploadTarget uploadTarget, GLint level, GLsizei bufSize,
                                                 void* pixels, const char* caller) {
        if (GetCompressedLevelFormat(textureObject, uploadTarget, level) == GL_NONE) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Texture level is not stored in a compressed format."));
            return;
        }

        const auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
        const SizeT imageSize =
            textureMipmapObject->GetMipmapCompressedByteSize(uploadTarget, static_cast<Uint>(level));
        const void* src = textureMipmapObject->MapMipmapCompressedImage(uploadTarget, static_cast<Uint>(level));
        if (!src || imageSize == 0) return;

        if (bufSize >= 0 && static_cast<SizeT>(bufSize) < imageSize) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
            return;
        }

        const auto& pixelPackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
        if (pixelPackBufferObject) {
            if (pixelPackBufferObject->IsMapped()) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel pack buffer is currently mapped."));
                return;
            }
            const SizeT offset = reinterpret_cast<SizeT>(pixels);
            const SizeT bufferSize = pixelPackBufferObject->GetSize();
            if (offset > bufferSize || imageSize > bufferSize - offset) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "Packing would write past the end of the pixel pack buffer."));
                return;
            }
            pixelPackBufferObject->UploadSubData({const_cast<void*>(src), imageSize}, offset);
            return;
        }

        // No pixel-store packing here on purpose: GL 4.6 core 8.11 says the pixel storage modes are
        // ignored for a compressed image, which is also the only way the round trip stays byte-exact.
        if (pixels) Memcpy(pixels, src, imageSize);
    }

    void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
        const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        // ValidateTextureUploadTarget records InvalidEnum itself; wrapping it in a second RecordError
        // would report one failure twice.
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        CopyCompressedTextureImageToClientOrPBO(textureObject, textureUploadTarget, level, -1, img, __func__);
    }

    void GenTextures_State(GLsizei n, GLuint* textures) {
        // ===================== Error Checking ==============================
        if (n < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GenTextures_State", "n must be non-negative"));
            return;
        }

        // ======================= Processing ================================
        Vector<Uint> textureNames;
        MG_State::pGLContext->GenTextureNames(n, textureNames);
        Memcpy(textures, textureNames.data(), n * sizeof(GLuint));
    }

    void DeleteTextures_State(GLsizei n, const GLuint* textures) {
        // ===================== Error Checking ==============================
        if (n < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "DeleteTextures_State", "n must be non-negative."));
            return;
        }

        if (!textures) {
            MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "DeleteTextures_State",
                                                                           "Texture names array cannot be null."));
            return;
        }

        // ======================= Processing ================================
        for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
            Uint textureName = textures[i];
            if (textureName == 0) continue;
            if (!MG_State::pGLContext->ValidateTextureName(textureName)) continue;
            MG_State::pGLContext->MarkTextureObjectForDeletion(textureName);
        }
    }

    Bool ValidateCopyTextureSubImage(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLint level,
                                     GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
                                     GLsizei depth, const char* caller);

    // The destination box of a copy has to lie inside the storage the copy actually WRITES, which
    // is the requested (uploadTarget, level) pair's - not level 0's.
    //
    // This exists because the general-purpose ValidateTextureSubImageOffsets bounds everything by
    // ITextureObject::GetBaseSize(), which is hardcoded to level 0 (TextureObject::GetBaseSize ->
    // GetTexelSize(0, 0)). CopyReadFramebufferIntoMipmapRegion, meanwhile, sizes its rows and
    // slices from GetMipmapTexelSize(uploadTarget, level) and memcpys into the exact-sized
    // std::vector MipmapStorage allocated for that level, with no clamp of its own. A box that is
    // legal at level 0 and out of range at level N therefore passed validation and wrote past the
    // end of the heap allocation - e.g. a 4x4 copy at offset (4,4) into level 2 of an 8x8x4
    // GL_RGBA8 array texture ran 24 bytes past a 64-byte buffer. Every level > 0 of every
    // mipmapped texture was reachable that way, and both entry points had been no-ops before, so
    // the whole exposure arrived with their implementation.
    static Bool ValidateCopySubImageRegionAtLevel(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                                  TextureUploadTarget uploadTarget, GLint level, GLint xoffset,
                                                  GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
                                                  GLsizei depth, const char* caller) {
        const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
        if (mipmapTexture == nullptr) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "The destination texture has no mipmap storage."));
            return false;
        }
        const IntVec3 levelSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
        // A level that was never defined reports a degenerate extent. GL 4.6 core 8.6 makes
        // copying into an undefined texture image INVALID_OPERATION, and it is also what keeps the
        // writer below from indexing an empty allocation.
        if (levelSize.x() <= 0 || levelSize.y() <= 0 || levelSize.z() <= 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "The requested texture level has no storage."));
            return false;
        }
        // Signed 64-bit sums: xoffset and width are both GLint and an application may pass values
        // whose sum overflows a GLint, which would otherwise compare as negative and pass.
        const Int64 lastX = static_cast<Int64>(xoffset) + static_cast<Int64>(width);
        const Int64 lastY = static_cast<Int64>(yoffset) + static_cast<Int64>(height);
        const Int64 lastZ = static_cast<Int64>(zoffset) + static_cast<Int64>(depth);
        if (xoffset < 0 || yoffset < 0 || zoffset < 0 || lastX > levelSize.x() || lastY > levelSize.y() ||
            lastZ > levelSize.z()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    std::format("The destination region does not lie inside level {} ({}x{}x{}).", level,
                                levelSize.x(), levelSize.y(), levelSize.z())));
            return false;
        }
        return true;
    }

    // The shared body of glCopyTexSubImage3D and glCopyTextureSubImage3D once the caller has
    // resolved the destination texture. `allowCubeFaceFromZOffset` is the ONE difference between
    // the two forms: the DSA form takes a cube map and selects the face with zoffset (GL 4.6 core
    // 8.6), while the target-taking form cannot even name a cube map here - GL_TEXTURE_CUBE_MAP is
    // not in glCopyTexSubImage3D's accepted-target list, its faces go through
    // glCopyTexSubImage2D - so for it zoffset is always a layer index.
    static void CopyTextureSubImage3DResolved(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                              GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
                                              GLint y, GLsizei width, GLsizei height, Bool allowCubeFaceFromZOffset,
                                              const char* caller) {
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (width < 0 || height < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Copy dimensions must be non-negative."));
            return;
        }

        // THE FACE MAPPING HAS TO HAPPEN BEFORE THE BOUNDS CHECK, not after it. A cube map stores
        // its six faces as six upload targets of ONE z-slice each, so its GetBaseSize().z() is 1 -
        // and the generic offset validator, whose z bound always comes from that, rejected every
        // zoffset in 1..5 with GL_INVALID_VALUE before the mapping below could run. Five of six
        // faces were unreachable through glCopyTextureSubImage3D even though the entry point
        // documents zoffset as the face selector (GL 4.6 core 8.6). The cube bound is the FACE
        // COUNT, which the generic validator has no way to express because its `depth` parameter
        // is the copy extent; glClearTexSubImage already special-cases the same shape.
        TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
        GLint sliceOffset = zoffset;
        if (allowCubeFaceFromZOffset && textureObject->GetTarget() == TextureTarget::TextureCubeMap) {
            const SizeT faceCount = textureObject->GetUploadTargets().size();
            if (zoffset < 0 || static_cast<SizeT>(zoffset) >= faceCount) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", caller,
                        "zoffset selects the cube map face and must be in [0, " + std::to_string(faceCount) + ")."));
                return;
            }
            uploadTarget = static_cast<TextureUploadTarget>(
                static_cast<SizeT>(TextureUploadTarget::CubeMapPositiveX) + static_cast<SizeT>(zoffset));
            sliceOffset = 0;
        }

        if (!ValidateCopySubImageRegionAtLevel(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset,
                                               width, height, /*depth=*/1, caller)) {
            return;
        }
        if (!FramebufferImpl::ValidateReadFramebufferForCopy(caller)) return;
        CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, yoffset, sliceOffset, x, y,
                                            width, height, caller);
    }

    // The same for the one-dimensional pair. A 1D level is {width, 1, 1}, so the y and z arms of
    // the check above are trivially satisfied and the x arm is the whole rule - which is exactly
    // the one that overflowed: level 2 of an 8-texel GL_RGBA8 1D texture is 8 bytes, and a 4-texel
    // copy at xoffset 4 wrote 16 bytes starting 16 bytes in, entirely outside the allocation.
    static void CopyTextureSubImage1DResolved(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                              GLint level, GLint xoffset, GLint x, GLint y, GLsizei width,
                                              const char* caller) {
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (width < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Copy dimensions must be non-negative."));
            return;
        }
        const TextureUploadTarget uploadTarget = GetPrimaryUploadTarget(textureObject);
        if (!ValidateCopySubImageRegionAtLevel(textureObject, uploadTarget, level, xoffset, /*yoffset=*/0,
                                               /*zoffset=*/0, width, /*height=*/1, /*depth=*/1, caller)) {
            return;
        }
        if (!FramebufferImpl::ValidateReadFramebufferForCopy(caller)) return;
        CopyReadFramebufferIntoMipmapRegion(textureObject, uploadTarget, level, xoffset, /*yoffset=*/0,
                                            /*zoffset=*/0, x, y, width, /*height=*/1, caller);
    }

    void CopyTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
                                 GLint y, GLsizei width, GLsizei height) {
        // GL 4.6 core 8.6 table: the three-dimensional form of the bound-texture copy accepts
        // exactly TEXTURE_3D, TEXTURE_2D_ARRAY and TEXTURE_CUBE_MAP_ARRAY. A cube map's faces are
        // two-dimensional targets of their own and go through glCopyTexSubImage2D.
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        if (textureTarget != TextureTarget::Texture3D && textureTarget != TextureTarget::Texture2DArray &&
            textureTarget != TextureTarget::TextureCubeMapArray) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "glCopyTexSubImage3D requires GL_TEXTURE_3D, GL_TEXTURE_2D_ARRAY or "
                                             "GL_TEXTURE_CUBE_MAP_ARRAY."));
            return;
        }
        const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
        if (!textureObject) return;
        CopyTextureSubImage3DResolved(textureObject, level, xoffset, yoffset, zoffset, x, y, width, height,
                                      /*allowCubeFaceFromZOffset=*/false, __func__);
    }

    // What the three CopyTextureSubImage forms check in common (GL 4.6 core 8.6), once the caller
    // has rejected an effective target its own form does not accept: the destination region has to
    // lie inside the level, and the read framebuffer has to be able to supply pixels at all.
    Bool ValidateCopyTextureSubImage(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLint level,
                                     GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height,
                                     GLsizei depth, const char* caller) {
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return false;
        if (width < 0 || height < 0 || depth < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Copy dimensions must be non-negative."));
            return false;
        }
        if (!TextureImpl::ValidateTextureSubImageOffsets(textureObject, xoffset, width, yoffset, height, zoffset,
                                                         depth)) {
            return false;
        }
        return FramebufferImpl::ValidateReadFramebufferForCopy(caller);
    }

    void CopyTexSubImage2D_Backend(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
                                   GLsizei width, GLsizei height) {
        MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
    }

    void CopyImageSubData_Backend(const MG_Backend::CopyImageEndpoint& src,
                                  GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
                                  const MG_Backend::CopyImageEndpoint& dst,
                                  GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
                                  GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
        auto copyImageSubData = MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData;
        if (!copyImageSubData) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Backend does not support image-to-image copies."));
            return;
        }
        copyImageSubData(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel, dstX,
                         dstY, dstZ, srcWidth, srcHeight, srcDepth);
    }

    namespace {
        // The eleven targets GL 4.6 core 18.3.2 accepts. GL_TEXTURE_BUFFER, the six cube FACE
        // enums and every PROXY enum all convert to a TextureTarget this frontend recognises,
        // so ValidateTextureTarget lets them through; here they are INVALID_ENUM.
        Bool ValidateCopyImageTarget(GLenum target, const char* endpointName) {
            switch (target) {
            case GL_RENDERBUFFER:
            case GL_TEXTURE_1D:
            case GL_TEXTURE_1D_ARRAY:
            case GL_TEXTURE_2D:
            case GL_TEXTURE_2D_ARRAY:
            case GL_TEXTURE_2D_MULTISAMPLE:
            case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
            case GL_TEXTURE_3D:
            case GL_TEXTURE_CUBE_MAP:
            case GL_TEXTURE_CUBE_MAP_ARRAY:
            case GL_TEXTURE_RECTANGLE:
                return true;
            default:
                break;
            }
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
                    std::format("{} is not a target glCopyImageSubData accepts as the {}.",
                                MG_Util::ConvertGLEnumToString(target), endpointName)));
            return false;
        }

        IntVec3 GetCopyImageLevelSize(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                      TextureUploadTarget uploadTarget, GLint level) {
            const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
            if (!mipmapTexture) return textureObject->GetBaseSize();
            return mipmapTexture->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
        }

        // glCopyImageSubData names an object that must already exist, and GL 4.6 core 18.3.2
        // spells the failure INVALID_VALUE - "if either name does not correspond to a valid
        // object". The shared ValidateTextureObject says INVALID_OPERATION, which is right for
        // the ~30 entry points that reach it through a BOUND object (where the name was never
        // in question and the fault is the binding), so this is a local rule rather than a
        // change to the helper.
        Bool ValidateCopyImageObjectExists(const MG_Backend::CopyImageEndpoint& endpoint,
                                           const char* endpointName) {
            if (endpoint.Exists()) return true;
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
                    std::format("The {} name does not correspond to an existing image object.", endpointName)));
            return false;
        }

        // Same split for the target/object disagreement: GL 4.6 core 18.3.2 makes a target that
        // does not match the object INVALID_ENUM, where the shared uniformity helper records
        // INVALID_OPERATION for the upload paths that share it.
        Bool ValidateCopyImageTargetMatchesObject(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                                  TextureTarget target, const char* endpointName) {
            if (!textureObject || textureObject->GetTarget() == target) return true;
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
                    std::format("The {} target {} does not match the target the object was created with ({}).",
                                endpointName, MG_Util::ConvertTextureTargetToString(target),
                                MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
            return false;
        }

        // ---- The questions ValidateCopyImageSubData_State asks of one endpoint. ---------------
        // A renderbuffer answers all of them directly: it has exactly one image, no mip chain and
        // no sampler state, and it carries its own internal format and extent.

        Int GetCopyImageEndpointSamples(const MG_Backend::CopyImageEndpoint& endpoint) {
            if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetSamples();
            return endpoint.Texture->GetSamples();
        }

        TextureInternalFormat GetCopyImageEndpointFormat(const MG_Backend::CopyImageEndpoint& endpoint) {
            if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->GetInternalFormat();
            return endpoint.Texture->GetFormat();
        }

        // A renderbuffer has level 0 and nothing else, and the failure is the same INVALID_VALUE
        // ValidateTextureLevelExists records for a level a texture does not have.
        Bool ValidateCopyImageEndpointLevelExists(const MG_Backend::CopyImageEndpoint& endpoint, GLint level,
                                                  const char* caller) {
            if (!endpoint.IsRenderbuffer()) {
                return TextureImpl::ValidateTextureLevelExists(endpoint.Texture, level, caller);
            }
            if (level == 0) return true;
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "A renderbuffer has only level 0."));
            return false;
        }

        // Targets with no mip chain have q == level_base by definition (GL 4.6 core 8.17), so no
        // minification filter can make them mipmap incomplete - while the shared predicate derives
        // q from the base level's size alone and would call a 16x16 multisample image incomplete.
        Bool CopyImageTargetHasMipmapChain(TextureTarget target) {
            switch (target) {
            case TextureTarget::TextureRectangle:
            case TextureTarget::TextureBuffer:
            case TextureTarget::Texture2DMultisample:
            case TextureTarget::Texture2DMultisampleArray:
                return false;
            default:
                return true;
            }
        }

        Bool IsCopyImageEndpointComplete(const MG_Backend::CopyImageEndpoint& endpoint) {
            // A renderbuffer is complete exactly when it has storage - there is nothing else it
            // could be missing.
            if (endpoint.IsRenderbuffer()) return endpoint.Renderbuffer->IsAllocated();
            const auto* texture = endpoint.Texture.get();
            if (!texture) return false;
            // 18.3.2 asks for TEXTURE completeness, which GL 4.6 core 8.17 defines to include the
            // MIP CHAIN whenever the minification filter samples it - and ITextureObject::
            // IsComplete() only answers the storage half (an internal format, and no zero-size
            // level in the middle of the chain). A texture with level 0 alone and the default
            // NEAREST_MIPMAP_LINEAR filter is incomplete, which is exactly how
            // KHR-GL43.copy_image.incomplete_tex builds its subject.
            //
            // The filter is the texture's OWN: copy-image never goes through a texture unit, so no
            // sampler object is in play. An immutable texture is unaffected - glTexStorage clamps
            // TEXTURE_MAX_LEVEL to levels-1, which is what makes a single-level immutable texture
            // mipmap complete under any filter.
            const auto& sampler = texture->GetSamplerObject();
            const Bool mipmapped = CopyImageTargetHasMipmapChain(texture->GetTarget()) && sampler &&
                                   sampler->GetMipmapMode() != SamplerMipmapMode::None;
            return MG_State::GLState::IsMipmapCompleteForFilter(texture, mipmapped);
        }

        GLenum GetCopyImageEndpointCompressedFormat(const MG_Backend::CopyImageEndpoint& endpoint,
                                                    TextureUploadTarget uploadTarget, GLint level) {
            if (endpoint.IsRenderbuffer()) return GL_NONE;
            return GetCompressedLevelFormat(endpoint.Texture, uploadTarget, level);
        }

        IntVec3 GetCopyImageEndpointLevelSize(const MG_Backend::CopyImageEndpoint& endpoint,
                                              TextureUploadTarget uploadTarget, GLint level) {
            if (endpoint.IsRenderbuffer()) {
                return {endpoint.Renderbuffer->GetWidth(), endpoint.Renderbuffer->GetHeight(), 1};
            }
            return GetCopyImageLevelSize(endpoint.Texture, uploadTarget, level);
        }

        // The per-axis extent of one endpoint's image AS THIS ENTRY POINT ADDRESSES IT, which is
        // not always the level extent this frontend stores.
        //
        // GL 4.6 core 18.3.2 treats EVERY array texture as a stack of slices addressed by z, and
        // gives a 1D array an image height of 1. This frontend stores a 1D array the way
        // glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) writes it instead - layers on y - so the
        // two views have to be told apart here. Measuring y against the LAYER count is what let
        // srcY = 14 on a 16-wide, 16-layer 1D array come back GL_NO_ERROR
        // (KHR-GL43.copy_image.exceeding_boundaries, the src_test_case y variants); the CTS is
        // unambiguous about the convention, forcing height = 1 for 1D and 1D_ARRAY and listing
        // 1D_ARRAY as multilayer.
        //
        // A CUBE MAP is the other target whose z bound is not the level extent: this frontend
        // keeps its six faces as six separate one-slice upload targets, so the level says 1 and
        // the real bound is 6. A cube-map ARRAY is one upload target whose depth already counts
        // layer-faces, and every remaining target is answered by the level extent verbatim.
        IntVec3 GetCopyImageEndpointRegionBounds(const MG_Backend::CopyImageEndpoint& endpoint,
                                                 const IntVec3& levelSize) {
            const TextureTarget target = (!endpoint.IsRenderbuffer() && endpoint.Texture)
                                             ? endpoint.Texture->GetTarget()
                                             : TextureTarget::Unknown;
            if (target == TextureTarget::TextureCubeMap) {
                return {levelSize.x(), levelSize.y(), 6};
            }
            if (target == TextureTarget::Texture1DArray) {
                return {levelSize.x(), 1, std::max(levelSize.y(), 1)};
            }
            return {levelSize.x(), levelSize.y(), std::max(levelSize.z(), 1)};
        }

        // GL 4.6 core 18.3.2 requires INVALID_VALUE when the region exceeds either image's
        // boundaries. The only bounds-shaped call this validator used to make was
        // ValidateCopyImageBlockAlignment, whose first line returns true for every UNCOMPRESSED
        // format - so no uncompressed copy was bounded at all, and the z extent could not be
        // bounded even in principle because srcZ/dstZ never reached the validator. Texture
        // endpoints were covered only by accident, through the ES driver's own error, which the
        // DirectGLES backend logs and swallows rather than reporting; a GL_RENDERBUFFER endpoint
        // got neither (KHR-GL43.copy_image.exceeding_boundaries).
        Bool ValidateCopyImageRegionBounds(const MG_Backend::CopyImageEndpoint& endpoint, const IntVec3& levelSize,
                                           GLint x, GLint y, GLint z, GLsizei width, GLsizei height, GLsizei depth,
                                           const char* endpointName) {
            // An extent this frontend does not know cannot bound anything, and guessing would
            // reject a copy GL allows. Every caller has already established that the level
            // exists and that the image is complete, so this is a belt-and-braces guard.
            if (levelSize.x() <= 0 || levelSize.y() <= 0) return true;
            const IntVec3 bounds = GetCopyImageEndpointRegionBounds(endpoint, levelSize);
            if (x >= 0 && y >= 0 && z >= 0 && static_cast<Int64>(x) + width <= bounds.x() &&
                static_cast<Int64>(y) + height <= bounds.y() && static_cast<Int64>(z) + depth <= bounds.z()) {
                return true;
            }
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", "ValidateCopyImageSubData_State",
                    std::format("The {} region [{}, {}, {}] + [{} x {} x {}] does not fit inside the {} x {} x {} "
                                "image.",
                                endpointName, x, y, z, width, height, depth, bounds.x(), bounds.y(), bounds.z())));
            return false;
        }
    } // namespace

    Bool ValidateCopyImageSubData_State(const MG_Backend::CopyImageEndpoint& src,
                                        GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
                                        const MG_Backend::CopyImageEndpoint& dst,
                                        GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
                                        GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
        if (!ValidateCopyImageObjectExists(src, "source") ||
            !ValidateCopyImageObjectExists(dst, "destination")) {
            return false;
        }
        // GL_RENDERBUFFER has no TextureTarget to convert to, and it needs none: it is its own
        // whole-image target, and the endpoint that carries it was resolved from the renderbuffer
        // namespace, so it matches its object by construction.
        const auto srcTextureTarget =
            src.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(srcTarget);
        const auto dstTextureTarget =
            dst.IsRenderbuffer() ? TextureTarget::Unknown : MG_Util::ConvertGLEnumToTextureTarget(dstTarget);
        if ((!src.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(srcTextureTarget)) ||
            (!dst.IsRenderbuffer() && !TextureImpl::ValidateTextureTarget(dstTextureTarget))) {
            return false;
        }
        // GL_TEXTURE_BUFFER and the cube FACE enums convert to a target this frontend knows, but
        // 18.3.2 does not accept them here - only the eleven whole-image targets do.
        if (!ValidateCopyImageTarget(srcTarget, "source") || !ValidateCopyImageTarget(dstTarget, "destination")) {
            return false;
        }
        if (!ValidateCopyImageTargetMatchesObject(src.Texture, srcTextureTarget, "source") ||
            !ValidateCopyImageTargetMatchesObject(dst.Texture, dstTextureTarget, "destination")) {
            return false;
        }
        if (!TextureImpl::ValidateTextureLevelNumber(srcLevel) ||
            !TextureImpl::ValidateTextureLevelNumber(dstLevel)) {
            return false;
        }
        // ValidateTextureLevelNumber only bounds the index by GL_MAX_TEXTURE_SIZE; it cannot
        // see that this particular texture stops at level 0. Both backends turn <level> into an
        // image subresource with no further checking (DirectVulkan builds a VkImageCopy from it,
        // DirectGLES forwards it to the ES copy), so a level the texture never had reached the
        // driver as an out-of-range mip index - on Adreno that is a SIGSEGV inside
        // vkCmdCopyImage, which is what KHR-GL43.copy_image.non_existent_mipmap used to do to
        // the whole glcts process. The answer the spec asks for is GL_INVALID_VALUE.
        if (!ValidateCopyImageEndpointLevelExists(src, srcLevel, __func__) ||
            !ValidateCopyImageEndpointLevelExists(dst, dstLevel, __func__)) {
            return false;
        }
        if (srcWidth < 0 || srcHeight < 0 || srcDepth < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Copy dimensions must be non-negative."));
            return false;
        }
        if (srcWidth == 0 || srcHeight == 0 || srcDepth == 0) {
            return false;
        }
        // A multisample image can only be copied to one with the same sample count, and a
        // single-sample image reports zero - so this one comparison is also what rejects
        // copying between a multisample target and a non-multisample one.
        const Int srcSamples = GetCopyImageEndpointSamples(src);
        const Int dstSamples = GetCopyImageEndpointSamples(dst);
        if (srcSamples != dstSamples) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", __func__,
                    std::format("The two images have different sample counts ({} vs. {}).",
                                srcSamples, dstSamples)));
            return false;
        }
        // 18.3.2: both images must be complete. An incomplete one has no defined texels to copy
        // and no defined storage to copy into.
        const Bool srcComplete = IsCopyImageEndpointComplete(src);
        const Bool dstComplete = IsCopyImageEndpointComplete(dst);
        if (!srcComplete || !dstComplete) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", __func__,
                    std::format("A copied image is incomplete (source complete: {}, destination complete: {}).",
                                srcComplete, dstComplete)));
            return false;
        }
        const auto srcUploadTarget = GetPrimaryUploadTarget(src.Texture);
        const auto dstUploadTarget = GetPrimaryUploadTarget(dst.Texture);
        const auto srcBlock = TextureImpl::ResolveCopyImageTexelBlock(
            GetCopyImageEndpointFormat(src), GetCopyImageEndpointCompressedFormat(src, srcUploadTarget, srcLevel));
        const auto dstBlock = TextureImpl::ResolveCopyImageTexelBlock(
            GetCopyImageEndpointFormat(dst), GetCopyImageEndpointCompressedFormat(dst, dstUploadTarget, dstLevel));
        if (!TextureImpl::ValidateCopyImageFormatCompatibility(srcBlock, dstBlock)) {
            return false;
        }
        const IntVec3 srcLevelSize = GetCopyImageEndpointLevelSize(src, srcUploadTarget, srcLevel);
        const IntVec3 dstLevelSize = GetCopyImageEndpointLevelSize(dst, dstUploadTarget, dstLevel);
        if (!TextureImpl::ValidateCopyImageBlockAlignment(srcBlock, srcX, srcY, srcWidth, srcHeight,
                                                          srcLevelSize.x(), srcLevelSize.y(), "source") ||
            !TextureImpl::ValidateCopyImageBlockAlignment(dstBlock, dstX, dstY, srcWidth, srcHeight,
                                                          dstLevelSize.x(), dstLevelSize.y(), "destination")) {
            return false;
        }
        // One region extent, measured against both images: GL 4.6 core 18.3.2 gives the copy a
        // single width/height/depth and requires it to fit in the source AND the destination.
        if (!ValidateCopyImageRegionBounds(src, srcLevelSize, srcX, srcY, srcZ, srcWidth, srcHeight, srcDepth,
                                           "source") ||
            !ValidateCopyImageRegionBounds(dst, dstLevelSize, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth,
                                           "destination")) {
            return false;
        }
        return true;
    }

    void CopyTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
        // The bound-texture form of glCopyTextureSubImage1D. GL 4.6 core 8.6 accepts only
        // GL_TEXTURE_1D here.
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        if (textureTarget != TextureTarget::Texture1D) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "glCopyTexSubImage1D requires GL_TEXTURE_1D."));
            return;
        }
        const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
        if (!textureObject) return;
        CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
    }

    Bool CopyTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
                              GLsizei height, GLint border) {
        auto internalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
        if (!ValidateTextureMutable(textureObject, __func__)) return false;

        const auto& currentReadFBO =
            MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
        if (!currentReadFBO) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyTexImage2D_State",
                                             "No framebuffer is currently bound to the GL_READ_FRAMEBUFFER target."));
            return false;
        }

        Bool isDepth = MG_Util::IsDepthFormatInternalFormat(internalFormat);
        Bool isStencil = MG_Util::IsStencilFormatInternalFormat(internalFormat);
        TextureInternalFormat srcInternalFormat = TextureInternalFormat::Unknown;
#define GET_SRC_INTERNAL_FORMAT(AttachmentType)                                                                        \
    const auto& srcAttachment = currentReadFBO->GetAttachment(AttachmentType);                                         \
    if (srcAttachment.IsTexture()) {                                                                                   \
        const auto& texObj = srcAttachment.GetTexture();                                                               \
        srcInternalFormat = texObj->GetFormat();                                                                       \
    } else if (srcAttachment.IsRenderbuffer()) {                                                                       \
        const auto& rboObj = srcAttachment.GetRenderbuffer();                                                          \
        srcInternalFormat = rboObj->GetInternalFormat();                                                               \
    } else {                                                                                                           \
        MG_State::pGLContext->RecordError(                                                                             \
            ErrorCode::InvalidOperation,                                                                               \
            MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyTexImage2D_State",                                     \
                                         "The attachment specified by the read buffer is incomplete."));               \
        return false;                                                                                                  \
    }
        if (isDepth && isStencil) {
            // A combined internalformat copies both halves, so the read framebuffer
            // must populate both attachment points.
            const auto& stencilAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Stencil);
            const auto& depthAttachment = currentReadFBO->GetAttachment(FramebufferAttachmentType::Depth);
            if (!depthAttachment.IsValid() || depthAttachment.IsEmpty() || !stencilAttachment.IsValid() ||
                stencilAttachment.IsEmpty()) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", "CopyTexImage2D_State",
                        "DEPTH_STENCIL copy requires both depth and stencil attachments in the read framebuffer."));
                return false;
            }
            GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
        } else if (isDepth) {
            GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Depth);
        } else if (isStencil) {
            GET_SRC_INTERNAL_FORMAT(FramebufferAttachmentType::Stencil);
        } else {
            const auto& readBufferType = currentReadFBO->GetReadBuffer();
            GET_SRC_INTERNAL_FORMAT(readBufferType);
        }

        // The validator has already recorded GL_INVALID_OPERATION; just decline. Throwing
        // here unwound a C++ exception through the C GL ABI and killed the process (see the
        // same reasoning at :604-609).
        if (!TextureImpl::ValidateCopyTexImageBaseFormatSubset(internalFormat, srcInternalFormat)) return false;

        GLenum outInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(srcInternalFormat);
        GLenum realInternalFormat = GL_RGBA8;
        GLenum format = GL_DEPTH_COMPONENT;
        GLenum type = GL_UNSIGNED_INT;
        MG_Util::TextureFormatProcessor::NormalizePixelFormat(outInternalFormat, PixelFormatNormalizeOptionBit::None,
                                                              &realInternalFormat, &format, &type);
        const auto& pixelUnpackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        TexImage2D_State(target, level, (GLint)realInternalFormat, width, height, border, format, type, nullptr);
        MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).Bind(pixelUnpackBufferObject);
        return true;
    }

    void CopyTexImage2D_Backend(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
                                GLsizei height, GLint border) {
        MG_Backend::gBackendFunctionsTable.GL.CopyTexImage2D(target, level, internalformat, x, y, width, height,
                                                             border);
    }

    void CopyTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
                              GLint border) {
        // 1D textures are not implemented by this backend set. Record the error the way every
        // other unsupported entry point does - throwing unwinds through the C GL ABI and kills
        // the process, which is never an acceptable answer to an unsupported call.
        MG_State::pGLContext->RecordError(
            ErrorCode::InvalidOperation,
            MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyTexImage1D",
                                         "1D textures are not supported by this implementation"));
    }

    // The three-dimensional twin of CompressedTexSubImage2D_State: a block-aligned box of the
    // compressed image the level shadows is replaced, slice by slice. Same deviation as the 2D form
    // - the uncompressed texel shadow beside it is NOT touched, so what changes is the image
    // glGetCompressedTexImage hands back, not what the level samples as.
    void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
                                       GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
                                       const void* data) {
        // ======================= Converting ================================
        const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        // Zero block width doubles as "format is not a specific compressed format", the
        // INVALID_ENUM case - one lookup answers both questions.
        const auto compressedInfo = MG_Util::GetCompressedFormatInfo(format);

        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        // A proxy holds no image to modify; only the glTexImage*/glCompressedTexImage* pair
        // accepts one.
        if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "A proxy target has no texture image to modify."));
            return;
        }
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
        if (width < 0 || height < 0 || depth < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "width, height and depth must be non-negative."));
            return;
        }
        if (compressedInfo.blockWidth == 0) {
            RecordUnsupportedCompressedFormat(__func__);
            return;
        }

        auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
        if (textureMipmapObject == nullptr) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
            return;
        }
        // GL 4.6 core 8.7: INVALID_OPERATION unless the image being modified is stored in
        // exactly this compressed format. That is also what makes the block arithmetic below
        // sound - the level's grid is measured with THIS format's block size.
        const GLenum levelFormat =
            textureMipmapObject->GetMipmapCompressedFormat(textureUploadTarget, static_cast<Uint>(level));
        if (levelFormat != format) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "format does not match the internal format of the texture image."));
            return;
        }

        const IntVec3 levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
        const Int levelDepth = std::max(levelSize.z(), 1);
        // Subtractions rather than sums for the reason CompressedTexSubImage2D_State spells out:
        // offset + extent are both application-supplied GLints and a signed overflow is undefined.
        if (xoffset < 0 || yoffset < 0 || zoffset < 0 || width > levelSize.x() - xoffset ||
            height > levelSize.y() - yoffset || depth > levelDepth - zoffset) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "The replaced region does not lie within the texture image."));
            return;
        }
        // GL 4.6 core 8.7 for block-based formats: the region must start on a block boundary
        // and must either be a whole number of blocks wide/high or run to the image's edge. Every
        // format that reaches here is 4x4x1, so the depth axis carries no block alignment rule -
        // each slice is its own block grid.
        const Int blockWidth = static_cast<Int>(compressedInfo.blockWidth);
        const Int blockHeight = static_cast<Int>(compressedInfo.blockHeight);
        const Bool alignedX = (xoffset % blockWidth == 0) &&
                              (width % blockWidth == 0 || xoffset + width == levelSize.x());
        const Bool alignedY = (yoffset % blockHeight == 0) &&
                              (height % blockHeight == 0 || yoffset + height == levelSize.y());
        if (!alignedX || !alignedY) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "The replaced region is not aligned to the format's compressed blocks."));
            return;
        }
        // Exactly the size the format and dimensions imply, which is also what keeps the copy
        // below in bounds.
        const SizeT expectedImageSize =
            MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth});
        if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "imageSize does not match the compressed image size."));
            return;
        }

        // ======================= Processing ================================
        if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
        const void* compressedBytes = CompressedUnpackSource(data);
        if (expectedImageSize == 0) return; // a zero-sized region is a legal no-op
        if (compressedBytes == nullptr) {
            // No unpack buffer and a null client pointer: there is nothing to read. GL leaves
            // this undefined rather than erroring, and dereferencing it is the one answer that
            // is never acceptable.
            MGLOG_D("%s: null data with no pixel unpack buffer bound, nothing to replace", __func__);
            return;
        }

        static std::atomic<Bool> announcedNoCodec3D{false};
        if (!announcedNoCodec3D.exchange(true)) {
            MGLOG_W("%s: the compressed blocks are stored verbatim and returned by "
                    "glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
                    "reach the texels this level SAMPLES as. Upload through glTexSubImage3D for "
                    "that.",
                    __func__);
        }

        const SizeT blobSize =
            textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level));
        const void* existing =
            textureMipmapObject->MapMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level));
        if (blobSize == 0 || existing == nullptr) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "The texture level holds no compressed image to modify."));
            return;
        }
        Vector<Uint8> blob(blobSize);
        Memcpy(blob.data(), existing, blobSize);

        const SizeT blockByteSize = compressedInfo.blockByteSize;
        const SizeT levelBlocksX = (static_cast<SizeT>(levelSize.x()) + compressedInfo.blockWidth - 1) /
                                   compressedInfo.blockWidth;
        const SizeT levelBlocksY = (static_cast<SizeT>(levelSize.y()) + compressedInfo.blockHeight - 1) /
                                   compressedInfo.blockHeight;
        const SizeT levelRowBytes = levelBlocksX * blockByteSize;
        const SizeT levelSliceBytes = levelRowBytes * levelBlocksY;
        const SizeT regionBlocksX = (static_cast<SizeT>(width) + compressedInfo.blockWidth - 1) /
                                    compressedInfo.blockWidth;
        const SizeT regionBlocksY = (static_cast<SizeT>(height) + compressedInfo.blockHeight - 1) /
                                    compressedInfo.blockHeight;
        const SizeT firstBlockX = static_cast<SizeT>(xoffset) / compressedInfo.blockWidth;
        const SizeT firstBlockY = static_cast<SizeT>(yoffset) / compressedInfo.blockHeight;
        const SizeT regionRowBytes = regionBlocksX * blockByteSize;
        const SizeT regionSliceBytes = regionRowBytes * regionBlocksY;
        const auto* source = static_cast<const Uint8*>(compressedBytes);
        for (SizeT slice = 0; slice < static_cast<SizeT>(depth); ++slice) {
            const SizeT destSliceBase = (static_cast<SizeT>(zoffset) + slice) * levelSliceBytes;
            for (SizeT row = 0; row < regionBlocksY; ++row) {
                const SizeT destOffset =
                    destSliceBase + (firstBlockY + row) * levelRowBytes + firstBlockX * blockByteSize;
                if (destOffset + regionRowBytes > blobSize) break; // a level whose blob predates its size
                Memcpy(blob.data() + destOffset, source + slice * regionSliceBytes + row * regionRowBytes,
                       regionRowBytes);
            }
        }
        textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level), format,
                                                      blob.data(), blobSize);
    }

    // Replaces a block-aligned rectangle of the compressed image glCompressedTexImage2D (or a
    // compressed glTexImage2D) shadowed for this level. Same deviation as the image call it
    // patches: the uncompressed texel shadow beside it is NOT touched, because there is no
    // BC/ETC codec here to decode the incoming blocks with - so what changes is the image
    // glGetCompressedTexImage hands back, not what the level samples as. Marking the texels
    // dirty would therefore only re-upload bytes that did not change.
    void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
                                       GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
        // ======================= Converting ================================
        const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        // Zero block width doubles as "format is not a specific compressed format", the
        // INVALID_ENUM case - one lookup answers both questions.
        const auto compressedInfo = MG_Util::GetCompressedFormatInfo(format);

        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        // A proxy holds no image to modify; only the glTexImage*/glCompressedTexImage* pair
        // accepts one.
        if (TextureImpl::IsProxyTextureTarget(textureUploadTarget)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "A proxy target has no texture image to modify."));
            return;
        }
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
        if (width < 0 || height < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "width and height must be non-negative."));
            return;
        }
        if (compressedInfo.blockWidth == 0) {
            RecordUnsupportedCompressedFormat(__func__);
            return;
        }

        auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        auto* textureMipmapObject = MG_State::GLState::AsMipmapTexture(textureObject.get());
        if (textureMipmapObject == nullptr) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
            return;
        }
        // GL 4.6 core 8.7: INVALID_OPERATION unless the image being modified is stored in
        // exactly this compressed format. That is also what makes the block arithmetic below
        // sound - the level's grid is measured with THIS format's block size.
        const GLenum levelFormat =
            textureMipmapObject->GetMipmapCompressedFormat(textureUploadTarget, static_cast<Uint>(level));
        if (levelFormat != format) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "format does not match the internal format of the texture image."));
            return;
        }

        const IntVec3 levelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, static_cast<Uint>(level));
        // Written as a subtraction rather than `xoffset + width > levelSize.x()`: both operands
        // are application-supplied GLints, so the sum is free to overflow, and a signed overflow
        // is undefined behaviour that a compiler may resolve by assuming the check passes.
        // levelSize is our own and non-negative, and the offsets are known non-negative by the
        // time the subtraction runs, so this form cannot wrap.
        if (xoffset < 0 || yoffset < 0 || width > levelSize.x() - xoffset || height > levelSize.y() - yoffset) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "The replaced region does not lie within the texture image."));
            return;
        }
        // GL 4.6 core 8.7 for block-based formats: the region must start on a block boundary
        // and must either be a whole number of blocks wide/high or run to the image's edge.
        const Int blockWidth = static_cast<Int>(compressedInfo.blockWidth);
        const Int blockHeight = static_cast<Int>(compressedInfo.blockHeight);
        const Bool alignedX = (xoffset % blockWidth == 0) &&
                              (width % blockWidth == 0 || xoffset + width == levelSize.x());
        const Bool alignedY = (yoffset % blockHeight == 0) &&
                              (height % blockHeight == 0 || yoffset + height == levelSize.y());
        if (!alignedX || !alignedY) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "The replaced region is not aligned to the format's compressed blocks."));
            return;
        }
        // Exactly the size the format and dimensions imply, which is also what keeps the copy
        // below in bounds.
        const SizeT expectedImageSize =
            MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1});
        if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "imageSize does not match the compressed image size."));
            return;
        }

        // ======================= Processing ================================
        if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
        const void* compressedBytes = CompressedUnpackSource(data);
        if (expectedImageSize == 0) return; // a zero-sized region is a legal no-op
        if (compressedBytes == nullptr) {
            // No unpack buffer and a null client pointer: there is nothing to read. GL leaves
            // this undefined rather than erroring, and dereferencing it is the one answer that
            // is never acceptable.
            MGLOG_D("%s: null data with no pixel unpack buffer bound, nothing to replace", __func__);
            return;
        }

        // Once per process: the call is about to succeed, and what it does is narrower than what
        // an application has every right to expect from it. Before this existed the call answered
        // GL_INVALID_ENUM, which was wrong but at least visible; a silent success that leaves the
        // sampled texels untouched is the kind of thing that costs a day to find from the other
        // end. MGLOG_W is the right level and now survives at INFO; it sat at MGLOG_I only
        // while the Log.h ordering compiled warnings out of the builds that ship.
        static std::atomic<Bool> announcedNoCodec{false};
        if (!announcedNoCodec.exchange(true)) {
            MGLOG_W("%s: the compressed blocks are stored verbatim and returned by "
                    "glGetCompressedTexImage, but there is no BC/ETC decoder here, so they do not "
                    "reach the texels this level SAMPLES as. Upload through glTexSubImage2D for "
                    "that.",
                    __func__);
        }

        // The level's compressed image is stored as one blob, so the rectangle is patched into
        // a copy of it and the whole thing handed back. Compressed sub-image uploads are not a
        // hot path, and this keeps the storage layer's compressed API to the two calls it has.
        const SizeT blobSize =
            textureMipmapObject->GetMipmapCompressedByteSize(textureUploadTarget, static_cast<Uint>(level));
        const void* existing =
            textureMipmapObject->MapMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level));
        if (blobSize == 0 || existing == nullptr) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "The texture level holds no compressed image to modify."));
            return;
        }
        Vector<Uint8> blob(blobSize);
        Memcpy(blob.data(), existing, blobSize);

        const SizeT blockByteSize = compressedInfo.blockByteSize;
        const SizeT levelBlocksX = (static_cast<SizeT>(levelSize.x()) + compressedInfo.blockWidth - 1) /
                                   compressedInfo.blockWidth;
        const SizeT levelRowBytes = levelBlocksX * blockByteSize;
        const SizeT regionBlocksX = (static_cast<SizeT>(width) + compressedInfo.blockWidth - 1) /
                                    compressedInfo.blockWidth;
        const SizeT regionBlocksY = (static_cast<SizeT>(height) + compressedInfo.blockHeight - 1) /
                                    compressedInfo.blockHeight;
        const SizeT firstBlockX = static_cast<SizeT>(xoffset) / compressedInfo.blockWidth;
        const SizeT firstBlockY = static_cast<SizeT>(yoffset) / compressedInfo.blockHeight;
        const SizeT regionRowBytes = regionBlocksX * blockByteSize;
        const auto* source = static_cast<const Uint8*>(compressedBytes);
        for (SizeT row = 0; row < regionBlocksY; ++row) {
            const SizeT destOffset = (firstBlockY + row) * levelRowBytes + firstBlockX * blockByteSize;
            if (destOffset + regionRowBytes > blobSize) break; // a level whose blob predates its size
            Memcpy(blob.data() + destOffset, source + row * regionRowBytes, regionRowBytes);
        }
        textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, static_cast<Uint>(level), format,
                                                      blob.data(), blobSize);
    }

    void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
                                       GLsizei imageSize, const void* data) {
        // TODO: implement compressed upload - see CompressedTexImage2D_State.
        RecordUnsupportedCompressedFormat(__func__);
    }

    // The three-dimensional twin of CompressedTexImage2D_State, and the same deviation applies: the
    // blocks are shadowed verbatim for glGetCompressedTexImage while the texels this level SAMPLES
    // as stay zero, because there is no BC/ETC decoder here. A 3D compressed image is a stack of
    // `depth` two-dimensional block grids - every format that reaches here has a 4x4x1 block - so
    // the blob layout is slice-major and CalculateCompressedTextureImageSize already multiplies by
    // depth.
    void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
                                    GLsizei depth, GLint border, GLsizei imageSize, const void* data) {
        // ======================= Converting ================================
        const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        // Zero block width doubles as "internalformat is not a specific compressed format", which is
        // the INVALID_ENUM case - one lookup answers both questions.
        const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);

        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
        if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return;
        if (!TextureImpl::ValidateTextureSizeRange(width, height, depth)) return;
        if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
        if (compressedInfo.blockWidth == 0) {
            RecordUnsupportedCompressedFormat(__func__);
            return;
        }
        // GL 4.6 core 8.7: imageSize must be exactly the size the format and dimensions imply,
        // otherwise INVALID_VALUE. This is also the guard that keeps the copy below in bounds.
        const SizeT expectedImageSize =
            MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, depth});
        if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "imageSize does not match the compressed image size."));
            return;
        }

        // Object resolution copied from TexImage3D_State rather than routed through
        // GetTextureObjectByTarget, for the reason CompressedTexImage2D_State gives: a proxy target
        // is legal here and only CreateOrReplaceProxyTextureObject gives it an object to answer the
        // level queries from.
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        const Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!ValidateTextureMutable(textureObject, __func__)) return;

        // ======================= Processing ================================
        const TextureInternalFormat textureInternalFormat =
            MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        textureObject->SetInternalFormat(textureInternalFormat);

        // A proxy records the format and nothing else - it must never take storage, and it must never
        // be tagged compressed, or GL_TEXTURE_COMPRESSED_IMAGE_SIZE on a proxy would stop being
        // INVALID_OPERATION.
        if (isProxy) return;

        const SizeT internalBpp =
            MG_Util::GetInternalBytesPerPixel(textureInternalFormat, TexturePixelDataType::UnsignedByte);
        const SizeT internalBytes =
            static_cast<SizeT>(width) * static_cast<SizeT>(height) * static_cast<SizeT>(depth) * internalBpp;

        auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
        // AllocateStorage clears any compressed image the level used to hold, so this must run before
        // SetMipmapCompressedImage re-arms it.
        textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});

        if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
        const void* compressedBytes = CompressedUnpackSource(data);
        textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
                                                      expectedImageSize);
        textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
    }

    void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
                                    GLint border, GLsizei imageSize, const void* data) {
        // ======================= Converting ================================
        const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        // Zero block width doubles as "internalformat is not a specific compressed format", which is
        // the INVALID_ENUM case - one lookup answers both questions.
        const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);

        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureSizeWithTextureUploadTarget(textureUploadTarget, width, height)) return;
        if (!TextureImpl::ValidateTextureSizeRange(width, height, 1)) return;
        if (!TextureImpl::ValidateTextureBorderNumber(border)) return;
        if (!TextureImpl::ValidateTextureLevelWithUploadTarget(textureUploadTarget, level)) return;
        if (compressedInfo.blockWidth == 0) {
            RecordUnsupportedCompressedFormat(__func__);
            return;
        }
        // GL 4.6 core 8.7: imageSize must be exactly the size the format and dimensions imply,
        // otherwise INVALID_VALUE. This is also the guard that keeps the copy below in bounds.
        const SizeT expectedImageSize =
            MG_Util::CalculateCompressedTextureImageSize(compressedInfo, {width, height, 1});
        if (imageSize < 0 || static_cast<SizeT>(imageSize) != expectedImageSize) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "imageSize does not match the compressed image size."));
            return;
        }

        // Object resolution copied from TexImage2D_State rather than routed through
        // GetTextureObjectByTarget: GL 4.6 core 8.7 lets a proxy target reach glCompressedTexImage2D,
        // and only CreateOrReplaceProxyTextureObject gives the proxy a fresh object to answer the
        // level queries from.
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        const Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->CreateOrReplaceProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!ValidateTextureMutable(textureObject, __func__)) return;

        // ======================= Processing ================================
        // Texel storage stays uncompressed, exactly the deviation the RGTC/BPTC/ETC2 arms of
        // ConvertGLEnumToTextureInternalFormat already document: neither backend has a BC/ETC codec
        // and TextureInternalFormat has no compressed enumerator, so the shadow keeps the "one
        // format, N bytes per texel" layout the backend upload sizing, glGenerateMipmap's
        // bytes-per-texel division and the pixel-store packer all rely on. The image therefore
        // samples as zeros. The application's bytes are kept beside it so glGetCompressedTexImage can
        // return the image *as stored*, which GL 4.6 core 8.11 requires and which no re-encode could
        // satisfy byte for byte.
        const TextureInternalFormat textureInternalFormat =
            MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        textureObject->SetInternalFormat(textureInternalFormat);

        // A proxy records the format and nothing else - it must never take storage, and it must never
        // be tagged compressed, or GL_TEXTURE_COMPRESSED_IMAGE_SIZE on a proxy would stop being
        // INVALID_OPERATION.
        if (isProxy) return;

        const SizeT internalBpp =
            MG_Util::GetInternalBytesPerPixel(textureInternalFormat, TexturePixelDataType::UnsignedByte);
        const SizeT internalBytes = static_cast<SizeT>(width) * static_cast<SizeT>(height) * internalBpp;

        auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
        // AllocateStorage clears any compressed image the level used to hold, so this must run before
        // SetMipmapCompressedImage re-arms it.
        textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, 1}, internalBytes});

        if (!ValidateCompressedUnpackBufferSource(data, expectedImageSize, __func__)) return;
        const void* compressedBytes = CompressedUnpackSource(data);
        textureMipmapObject->SetMipmapCompressedImage(textureUploadTarget, level, internalformat, compressedBytes,
                                                      expectedImageSize);
        textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
    }

    void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
                                    GLsizei imageSize, const void* data) {
        const auto textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
        if (!ValidateTextureMutable(textureObject, __func__)) return;

        // TODO: implement compressed upload - see CompressedTexImage2D_State.
        RecordUnsupportedCompressedFormat(__func__);
    }

    void BindTexture_State(GLenum target, GLuint texture) {
        const Int activeUnit = MG_State::pGLContext->GetActiveTextureUnit();
        MGLOG_D("BindTexture_State called with target: 0x%X, texture: %u, unit: %d", target, texture, activeUnit);
        // ======================= Converting ================================
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);

        // ===================== Error Checking ==============================
        if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;

        // GL 3.3 core 3.8: name 0 is the target's default texture object - a real texture that
        // glTexImage*/glTexParameter*/glGetTex* must operate on - not "nothing bound". Binding it
        // restores the unit/target slot to its initial state.
        if (texture == 0) {
            auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit);
            auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
            const Bool changed = bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget));
            MG_State::pGLContext->NoteTextureUnitTouched(activeUnit, changed);
            return;
        }

        // Some desktop-side helper code saves GL_ACTIVE_TEXTURE and later feeds it back into glBindTexture
        // as if it were a texture name. Treating that as a no-op preserves the previous "invalid bind does not
        // change texture state" behavior, but avoids poisoning the error state every frame.
        if (!MG_State::pGLContext->ValidateTextureName(texture) && texture >= GL_TEXTURE0 && texture <= GL_TEXTURE31) {
            return;
        }

        // GL 3.3 core 3.8.1: a name that GenTextures never returned - or that has since been deleted -
        // is not a legal bind target in the core profile (no application-generated names), and the error
        // is INVALID_OPERATION, not INVALID_VALUE.
        if (!MG_State::pGLContext->ValidateTextureName(texture)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BindTexture_State", "Invalid texture name"));
            return;
        }

        // ======================= Processing ================================
        Bool doesTextureExist = MG_State::pGLContext->ValidateTextureObject(texture);
        if (!doesTextureExist) {
            MG_State::pGLContext->CreateTextureObject(texture, textureTarget);
        }
        auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);

        // ===================== Error Checking ==============================
        if (doesTextureExist && !TextureImpl::ValidateTextureTargetUniformity(textureObject, textureTarget)) return;

        // ======================= Processing ================================
        auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget);
        const Bool changed = bindingSlot.Bind(textureObject);
        MG_State::pGLContext->NoteTextureUnitTouched(MG_State::pGLContext->GetActiveTextureUnit(), changed);
    }

    void ActiveTexture_State(GLenum texture) {
        // ===================== Error Checking ==============================
        // GL 3.3 core 3.8: the valid range is [GL_TEXTURE0, GL_TEXTURE0 +
        // GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS) - NOT a fixed 0..31 range. GL CTS's per-case
        // state reset iterates every advertised combined unit, so rejecting units the
        // implementation itself reports would leave a sticky GL_INVALID_ENUM behind and abort
        // whole test batches. The backend already clamps its advertised value to the state
        // layer's MAX_TEXTURE_IMAGE_UNITS capacity.
        Int maxCombinedUnits = static_cast<Int>(MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
        if (MG_Backend::pActiveBackendObject) {
            maxCombinedUnits = std::min(
                maxCombinedUnits, MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits);
        }
        if (texture < GL_TEXTURE0 || static_cast<Int>(texture - GL_TEXTURE0) >= maxCombinedUnits) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", "ActiveTexture_State",
                    std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to {}, but got "
                                "invalid enum: 0x{:X}, which may stand for unit {}.",
                                maxCombinedUnits - 1, texture, texture - GL_TEXTURE0)));
            return;
        }

        // ======================= Processing ================================
        const Int unit = (Int)texture - GL_TEXTURE0;
        MGLOG_D("ActiveTexture_State: unit = %d", unit);
        MG_State::pGLContext->SetActiveTextureUnit(unit);
    }

    void GetTexImage_Backend(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
        MG_Backend::gBackendFunctionsTable.GL.GetTexImage(target, level, format, type, pixels);
    }

    // Add to GL_Texture.cpp
    // The half of the GetTexImage/GetTextureImage error set (GL 4.6 core 8.11) that depends on the
    // resolved texture object rather than on how it was named. Shared because the by-name entry
    // point does not route through GetTexImage_State and so used to enforce none of it.
    // A cube map's six faces are six independent images, and both readback spellings name one of
    // them: glGetTexImage through the TARGET token, glGetTextureSubImage through zoffset. Both then
    // have to tell the size checks below that ONE image is coming back, not six.
    static Bool IsCubeMapFaceUploadTarget(TextureUploadTarget target) {
        return target >= TextureUploadTarget::CubeMapPositiveX && target <= TextureUploadTarget::CubeMapNegativeZ;
    }

    // `imagesQueried` is how many of the texture's upload-target images the query hands back, and
    // exists for the destination-size check at the bottom. Zero means "all of them", which is what
    // the whole-level forms return - every face of a cube map. glGetTextureSubImage naming ONE cube
    // face passes 1: sizing that request against six faces' worth would reject the only buffer a
    // single-face read has any reason to pass.
    Bool ValidateTextureImageQuery(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject, GLint level,
                                   TextureInputFormat textureInputFormat, TexturePixelDataType texturePixelDataType,
                                   GLsizei bufSize, const void* pixels, const char* caller,
                                   SizeT imagesQueried = 0) {
        if (!TextureImpl::ValidateTextureObject(textureObject)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "No valid texture bound to target"));
            return false;
        }

        // A multisample texture has per-sample data with no single image to return, and a buffer
        // texture's data lives in the buffer object - neither target is in the accepted list.
        const auto target = textureObject->GetTarget();
        if (target == TextureTarget::Texture2DMultisample || target == TextureTarget::Texture2DMultisampleArray ||
            target == TextureTarget::TextureBuffer) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Texture target has no image to read back."));
            return false;
        }

        // Level range. The by-target path would reach these again inside
        // CopyTextureImageToClientOrPBO_State, but the by-name path on a backend that answers
        // GetTextureImage itself never gets there.
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return false;
        if (target == TextureTarget::TextureRectangle && level != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Level must be zero for rectangle textures"));
            return false;
        }

        // GL 4.6 core 8.11.4 names cube completeness as the only completeness a readback requires,
        // and for a cube map that is exactly what IsComplete() answers (all six faces defined at
        // every level). It must not speak for any other target: on a mip chain it also rejects
        // "level N defined, the levels below it not", which is a perfectly readable texture at
        // level N - and the shape glClearTexImage's conformance cases build, since they define
        // only the level they clear. The requested level's own existence is checked below.
        if ((target == TextureTarget::TextureCubeMap || target == TextureTarget::TextureCubeMapArray) &&
            !textureObject->IsComplete()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture is incomplete"));
            return false;
        }

        // Check PBO state
        const auto& pixelPackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();

        if (pixelPackBufferObject) {
            // Check if PBO is mapped
            if (pixelPackBufferObject->IsMapped()) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel pack buffer is currently mapped"));
                return false;
            }

            // Check alignment
            const SizeT typeSize = MG_Util::GetTexturePixelDataTypeSize(texturePixelDataType);
            if (typeSize != 0 && reinterpret_cast<uintptr_t>(pixels) % typeSize != 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                 "Pixel data not aligned for pixel pack buffer"));
                return false;
            }
        }

        // Shared format/type/internal-format matrix (packed-type pairing, depth-vs-color mismatch,
        // integer-ness). Also rejects a STENCIL_INDEX readback of anything but stencil-only
        // storage, which is the only pairing GL 4.4 / ARB_texture_stencil8 ever made legal.
        if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(
                textureInputFormat, textureObject->GetFormat(), texturePixelDataType)) {
            return false;
        }

        // GetTexImage-specific: DEPTH_STENCIL readback needs a depth-stencil texture (a depth-only
        // texture has no stencil data to return).
        if (textureInputFormat == TextureInputFormat::DepthStencil &&
            textureObject->GetFormat() != TextureInternalFormat::DepthStencil &&
            textureObject->GetFormat() != TextureInternalFormat::Depth24Stencil8 &&
            textureObject->GetFormat() != TextureInternalFormat::Depth32FStencil8) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "DEPTH_STENCIL readback requires a depth-stencil texture"));
            return false;
        }

        // The destination has to be big enough. This has to happen here rather than after the read
        // has been packed: any of the reasons the read can bail out early - an unmapped level, a
        // pack step that declines the format - would otherwise swallow the error entirely.
        if (textureObject->GetStorageType() == TextureStorageType::Mipmap) {
            const auto* textureMipmapObject =
                static_cast<const MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
            const auto& uploadTargets = textureObject->GetUploadTargets();
            // The half of the completeness gate above that GL does keep: the REQUESTED level has
            // to hold an image. A name that was never given one carries no levels at all (which is
            // also what an Unknown internal format answers), and a chain grown to reach level N
            // leaves every level below it at {0, 0, 0}.
            if (uploadTargets.empty() || static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
                return false;
            }
            const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTargets[0], level);
            if (texelSize.x() <= 0 || texelSize.y() <= 0 || texelSize.z() <= 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level has no image to read back."));
                return false;
            }

            // Tightly packed, and summed over every face because a whole-level cube map query
            // returns all six - unless the caller named a single face, which is what a non-zero
            // imagesQueried says. Pack pixel-store state only ever grows this, so a request
            // rejected here could not have fit under any packing.
            const SizeT imageCount = imagesQueried != 0 ? imagesQueried : uploadTargets.size();
            const SizeT required = MG_Util::CalculateInputTextureImageSize(textureInputFormat,
                                                                           texturePixelDataType, texelSize) *
                                   imageCount;

            if (bufSize >= 0 && static_cast<SizeT>(bufSize) < required) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
                return false;
            }

            if (pixelPackBufferObject) {
                const SizeT bufferSize = pixelPackBufferObject->GetSize();
                const SizeT offset = reinterpret_cast<SizeT>(pixels);
                if (offset > bufferSize || required > bufferSize - offset) {
                    MG_State::pGLContext->RecordError(
                        ErrorCode::InvalidOperation,
                        MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                                     "Packing would write past the end of the pixel pack buffer."));
                    return false;
                }
            }
        }

        return true;
    }

    Bool GetTexImage_State(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
        // ======================= Converting ================================
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        TextureInputFormat textureInputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
        TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);

        // ===================== Error Checking ==============================
        // Validate target
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexImage_State", "Invalid texture target"));
            return false;
        }

        // Validate level
        if (level < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexImage_State", "Level must be non-negative"));
            return false;
        }

        // Validate format
        if (!TextureImpl::ValidateTextureInputFormat(textureInputFormat)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexImage_State", "Invalid format"));
            return false;
        }

        // Validate type
        if (!TextureImpl::ValidateTexturePixelDataType(texturePixelDataType)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetTexImage_State", "Invalid pixel data type"));
            return false;
        }

        // Get texture object
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        Bool isProxy = TextureImpl::IsProxyTextureTarget(textureUploadTarget);
        auto& textureObject =
            isProxy ? TextureImpl::pProxyTextureManager->GetProxyTextureObject(textureUploadTarget)
                    : bindingSlot.GetBoundObject();

        // glGetTexImage has no bufSize argument: -1 stands for "no client-side limit". That skips
        // the destination-size branch but NOT the pixel-pack-buffer one, which measures the same
        // `required` against the bound PBO's real size - so a cube FACE query has to say it returns
        // one image here too, or a PBO sized for the one face this call packs is refused as too
        // small while the copy that follows writes exactly that much into it.
        return ValidateTextureImageQuery(textureObject, level, textureInputFormat, texturePixelDataType, -1, pixels,
                                         "GetTexImage_State",
                                         IsCubeMapFaceUploadTarget(textureUploadTarget) ? 1u : 0u);
    }

    // What this helper can and cannot answer.
    //
    // ProcessTexturePixelsDataPack performs NO format or type conversion: it sizes every texel with
    // GetInternalBytesPerPixel(the TEXTURE's internal format) and memcpys the shadow rows verbatim,
    // and it carries a standing TODO for the pixel-store parameters, so it honours only SwapBytes and
    // the bitmap LSBFirst path. Both facts are invisible from the outside, and both are dangerous:
    //
    //   * a (format, type) narrower than the shadow's own texel makes the copy write MORE bytes than
    //     the caller's buffer holds. glGetTexImage passes bufSize = -1 (it has no bufSize argument),
    //     so the size guard below is skipped and the Memcpy runs off the end of the application's
    //     allocation - reading an 8x8 GL_RGBA8 level as (GL_RED, GL_UNSIGNED_BYTE) writes 256 bytes
    //     into the 64 that GL 4.6 core 8.11 says are required. A wider (format, type) is not an
    //     overflow but is still wrong data.
    //   * a pack state that puts padding, a row-length override or a skip offset between rows is
    //     ignored outright, so the rows land at the wrong destination strides - while the GPU
    //     readback path (DirectGLES StoreClientRows, and DirectVulkan through it) honours all of it.
    //     Same glGetTexImage call, two different destination layouts, decided by whether the texture
    //     happens to have a GPU image.
    //
    // So the copy is only correct when the client layout IS the shadow layout and the destination
    // walk is tight. That is checked here rather than assumed, and a request outside it is refused
    // with an error instead of being answered wrongly. Refusing is a real narrowing of what GL
    // promises - the spec wants the conversion performed - but the alternative on this path is a
    // heap overflow, and the conversion belongs in the pack processor rather than in another
    // open-coded copy here.
    static Bool ValidateShadowReadbackLayout(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                             TextureInputFormat textureInputFormat,
                                             TexturePixelDataType texturePixelDataType, GLsizei width,
                                             const char* caller) {
        const SizeT shadowTexelSize =
            MG_Util::GetInternalBytesPerPixel(textureObject->GetFormat(), texturePixelDataType);
        const SizeT clientTexelSize = MG_Util::GetInputBytesPerPixel(textureInputFormat, texturePixelDataType);
        if (shadowTexelSize == 0 || clientTexelSize == 0 || shadowTexelSize != clientTexelSize) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    std::format("Reading this texture back needs a format/type conversion that the CPU-shadow "
                                "path cannot perform: the shadow texel is {} bytes and the requested one is {}.",
                                shadowTexelSize, clientTexelSize)));
            return false;
        }

        // A tight destination walk is the only one the pack processor produces. GL_PACK_ALIGNMENT
        // defaults to 4, so a row whose byte count is not already a multiple of it needs padding that
        // would never be written - no glPixelStorei call from the application is required to reach
        // this.
        const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false);
        const SizeT alignment = packParams.Alignment > 0 ? static_cast<SizeT>(packParams.Alignment) : 1;
        const SizeT rowBytes = static_cast<SizeT>(std::max<GLsizei>(width, 0)) * clientTexelSize;
        const Bool tightRows = (rowBytes % alignment) == 0;
        const Bool noOverrides = packParams.RowLength == 0 && packParams.ImageHeight == 0 &&
                                 packParams.SkipPixels == 0 && packParams.SkipRows == 0 &&
                                 packParams.SkipImages == 0;
        if (!tightRows || !noOverrides) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    "The CPU-shadow readback path packs rows tightly and cannot honour a pixel-store state that "
                    "adds row padding, a row-length override or a skip offset."));
            return false;
        }
        return true;
    }

    void CopyTextureImageToClientOrPBO_State(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                            TextureUploadTarget textureUploadTarget, GLint level, GLenum format,
                                            GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
        if (!textureObject) return;

        TextureInputFormat textureInputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
        TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
        if (!TextureImpl::ValidateTextureInputFormat(textureInputFormat)) return;
        if (!TextureImpl::ValidateTexturePixelDataType(texturePixelDataType)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture storage is not mipmap-backed."));
            return;
        }

        auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        if (static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Texture level is out of range."));
            return;
        }

        const auto texelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level);
        if (!ValidateShadowReadbackLayout(textureObject, textureInputFormat, texturePixelDataType, texelSize.x(),
                                          caller)) {
            return;
        }

        const void* src = textureMipmapObject->MapMipmapData(textureUploadTarget, level);
        if (!src) return;

        SizeT packedSize = 0;
        void* packedPixels = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataPack(
            src, MG_State::pGLContext->GetPixelStoreParameters(false), textureObject->GetFormat(), texturePixelDataType,
            textureInputFormat, texturePixelDataType, texelSize, false, packedSize);
        if (!packedPixels || packedSize == 0) {
            if (packedPixels) free(packedPixels);
            return;
        }

        if (bufSize >= 0 && static_cast<SizeT>(bufSize) < packedSize) {
            free(packedPixels);
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Destination buffer is too small."));
            return;
        }

        const auto& pixelPackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject();
        if (pixelPackBufferObject) {
            const SizeT offset = reinterpret_cast<SizeT>(pixels);
            if (offset + packedSize > pixelPackBufferObject->GetSize()) {
                free(packedPixels);
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "Pixel pack buffer is too small."));
                return;
            }
            pixelPackBufferObject->UploadSubData({packedPixels, packedSize}, offset);
        } else if (pixels) {
            Memcpy(pixels, packedPixels, packedSize);
        }

        free(packedPixels);
    }

    /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
    void CreateTextures(GLenum target, GLsizei n, GLuint* textures) {
        if (n < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
            return;
        }
        if (n > 0 && !textures) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture output pointer cannot be null."));
            return;
        }

        switch (target) {
        case GL_TEXTURE_1D:
        case GL_TEXTURE_2D:
        case GL_TEXTURE_3D:
        case GL_TEXTURE_1D_ARRAY:
        case GL_TEXTURE_2D_ARRAY:
        case GL_TEXTURE_RECTANGLE:
        case GL_TEXTURE_CUBE_MAP:
        case GL_TEXTURE_CUBE_MAP_ARRAY:
        case GL_TEXTURE_BUFFER:
        case GL_TEXTURE_2D_MULTISAMPLE:
        case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
            break;
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Invalid texture target."));
            return;
        }

        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;

        Vector<Uint> textureNames;
        MG_State::pGLContext->GenTextureNames(n, textureNames);
        for (GLsizei i = 0; i < n; ++i) {
            textures[i] = textureNames[i];
            MG_State::pGLContext->CreateTextureObject(textureNames[i], textureTarget);
        }
    }

    // Shared front half of glTextureStorage1D/2D/3D. `dimension` selects which set of targets the
    // entry point accepts; `depth` is 1 for the lower-dimensional forms.
    static Bool ValidateTextureStorageShape(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                            int dimension, GLsizei levels, GLsizei width, GLsizei height,
                                            GLsizei depth, const char* caller) {
        if (levels < 1) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "levels must be positive."));
            return false;
        }
        // Immutable storage has to describe a real image, so unlike glTexImage*D a zero extent is
        // out of range rather than a legal empty level (GL 4.6 core 8.19).
        if (width < 1 || height < 1 || depth < 1) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "width, height and depth must be positive."));
            return false;
        }
        if (!IsTextureStorageTargetForDimension(textureObject->GetTarget(), dimension)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    std::format("The effective target {} is not accepted by this entry point.",
                                MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
            return false;
        }
        const Uint maxLevels = MaxTextureStorageLevels(textureObject->GetTarget(), width, height, depth);
        if (static_cast<Uint>(levels) > maxLevels) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    std::format("levels {} exceeds the {} the level-zero size admits.", levels, maxLevels)));
            return false;
        }
        return true;
    }

    void TextureStorage1D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
        if (!ValidateTextureStorageShape(textureObject, 1, levels, width, 1, 1, __func__)) return;
        if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
            return;
        }
        if (!ValidateTextureMutable(textureObject, __func__)) return;

        const auto textureUploadTarget = GetPrimaryUploadTarget(textureObject);
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());

        textureObject->SetInternalFormat(textureInternalFormat);
        for (GLsizei level = 0; level < levels; ++level) {
            const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
            const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, 1, 1);
            textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
            textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
            if (IsCompressedGLInternalFormat(internalformat)) {
                // After AllocateStorage, which clears the tag. See TexImage1D_State: no compressed
                // format has a 1D block layout, but glClearTexImage still has to refuse the request.
                textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
                                                                        static_cast<Uint>(level), internalformat);
            }
        }
        // Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
        // longer pre-existing chain has to be dropped explicitly.
        textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
        textureObject->SetImmutableLevels(static_cast<Uint>(levels));
        SeedImmutableViewState(textureObject, static_cast<Uint>(levels));
    }

    void TextureStorage2D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
        if (!ValidateTextureStorageShape(textureObject, 2, levels, width, height, 1, __func__)) return;
        if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
            return;
        }
        if (!ValidateTextureMutable(textureObject, __func__)) return;
        if (textureObject->GetTarget() == TextureTarget::TextureCubeMap && width != height) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Cube map immutable storage must be square."));
            return;
        }

        auto textureUploadTarget = GetPrimaryUploadTarget(textureObject);
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());

        GLenum realInternalFormat = internalformat;
        GLenum realFormat = GL_RGBA;
        GLenum realType = GL_UNSIGNED_BYTE;
        MG_Util::TextureFormatProcessor::NormalizePixelFormat(
            MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat), PixelFormatNormalizeOptionBit::None,
            &realInternalFormat, &realFormat, &realType);
        const SizeT bytesPerPixel = MG_Util::GetInternalBytesPerPixel(
            textureInternalFormat, MG_Util::ConvertGLEnumToTexturePixelDataType(realType));

        textureObject->SetInternalFormat(textureInternalFormat);
        // A cube map has six upload targets and glTexStorage2D allocates all of them at once (GL 4.6
        // core 8.19). Allocating only the primary one left the object cube-incomplete, so every
        // framebuffer it was attached to reported GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT. Every other
        // 2D target has exactly one upload target, so this loop is a no-op change for them.
        // A specific compressed internalformat commits every level it allocates to that
        // format, the same way glTexImage2D does - and here it matters twice over, because
        // immutable storage plus glCompressedTexSubImage2D IS the modern way to upload a
        // compressed texture: without the tag that sub-image call finds an uncompressed
        // level and refuses it. Zero width means a generic (implementation's choice)
        // format, which MobileGL answers with uncompressed storage, so it is not tagged.
        const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
        for (const auto uploadTarget : textureObject->GetUploadTargets()) {
            for (GLsizei level = 0; level < levels; ++level) {
                const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
                // GL 4.6 core 8.19: for GL_TEXTURE_1D_ARRAY the state-side HEIGHT is the LAYER
                // COUNT, and layers do not halve down the mip chain - level i is
                // (max(1, width >> i), height). Shrinking it made every mipmapped 1D array
                // level report fewer layers than it has.
                const Bool heightIsLayerCount = textureObject->GetTarget() == TextureTarget::Texture1DArray;
                const GLsizei levelHeight =
                    heightIsLayerCount ? height : std::max<GLsizei>(1, height >> level);
                const SizeT byteSize =
                    static_cast<SizeT>(levelWidth) * static_cast<SizeT>(levelHeight) * bytesPerPixel;
                textureMipmapObject->AllocateStorage(uploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
                textureMipmapObject->MarkStorageDirty(uploadTarget, level, false);
                if (compressedInfo.blockWidth != 0) {
                    // After AllocateStorage, which clears the tag.
                    textureMipmapObject->SetMipmapCompressedImage(
                        uploadTarget, static_cast<Uint>(level), internalformat, nullptr,
                        MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
                                                                     {levelWidth, levelHeight, 1}));
                }
                if (IsCompressedGLInternalFormat(internalformat)) {
                    // Also after AllocateStorage. The generic enums land here and nowhere above,
                    // and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
                    textureMipmapObject->SetMipmapRequestedCompressedFormat(uploadTarget,
                                                                            static_cast<Uint>(level), internalformat);
                }
            }
            // See TextureStorage1D.
            textureMipmapObject->TruncateMipmapLevels(uploadTarget, static_cast<Uint>(levels));
        }
        textureObject->SetImmutableLevels(static_cast<Uint>(levels));
        SeedImmutableViewState(textureObject, static_cast<Uint>(levels));
    }

    void TextureStorage3D(GLuint texture, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
                          GLsizei depth) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;
        if (textureObject->GetTarget() == TextureTarget::Texture3D &&
            IsCompressedGLInternalFormat(internalformat)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", __func__,
                    std::format("{} is a compressed internal format and cannot back GL_TEXTURE_3D storage.",
                                MG_Util::ConvertGLEnumToString(internalformat))));
            return;
        }
        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        if (!ValidateTextureStorageInternalFormat(textureInternalFormat, __func__)) return;
        if (!ValidateTextureStorageShape(textureObject, 3, levels, width, height, depth, __func__)) return;
        if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
            return;
        }
        if (!ValidateTextureMutable(textureObject, __func__)) return;
        const auto textureUploadTarget = GetPrimaryUploadTarget(textureObject);
        // The cube-array shape rules, shared with glTexImage3D / glCompressedTexImage3D so the
        // three cannot drift (they had: this check used to exist here and nowhere else).
        if (!TextureImpl::ValidateCubeMapArrayShape(textureUploadTarget, width, height, depth, __func__)) return;

        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());

        textureObject->SetInternalFormat(textureInternalFormat);
        // Array targets keep their layer count constant across levels; only true 3D
        // textures halve depth per level (GL 3.3 §3.9 glTexStorage3D).
        const Bool depthMips = DepthParticipatesInMipmapping(textureObject->GetTarget());
        // The same specific-compressed-format tag glTexStorage2D records, for the array targets a
        // compressed glTexStorage3D is legal on (GL_TEXTURE_3D was refused above). Zero width means
        // a generic format, which MobileGL answers with uncompressed storage, so it is not tagged.
        const auto compressedInfo = MG_Util::GetCompressedFormatInfo(internalformat);
        for (GLsizei level = 0; level < levels; ++level) {
            const GLsizei levelWidth = std::max<GLsizei>(1, width >> level);
            const GLsizei levelHeight = std::max<GLsizei>(1, height >> level);
            const GLsizei levelDepth = depthMips ? std::max<GLsizei>(1, depth >> level) : depth;
            const SizeT byteSize = ComputeTextureStorageByteSize(textureInternalFormat, levelWidth, levelHeight,
                                                                 levelDepth);
            textureMipmapObject->AllocateStorage(textureUploadTarget, level,
                                                 {{levelWidth, levelHeight, levelDepth}, byteSize});
            textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
            if (compressedInfo.blockWidth != 0) {
                // After AllocateStorage, which clears the tag.
                textureMipmapObject->SetMipmapCompressedImage(
                    textureUploadTarget, static_cast<Uint>(level), internalformat, nullptr,
                    MG_Util::CalculateCompressedTextureImageSize(compressedInfo,
                                                                 {levelWidth, levelHeight, levelDepth}));
            }
            if (IsCompressedGLInternalFormat(internalformat)) {
                // Also after AllocateStorage. The generic enums land here and nowhere above,
                // and glClearTexImage has to refuse them too (GL 4.6 core 8.19).
                textureMipmapObject->SetMipmapRequestedCompressedFormat(textureUploadTarget,
                                                                        static_cast<Uint>(level), internalformat);
            }
        }
        // See TextureStorage1D.
        textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
        textureObject->SetImmutableLevels(static_cast<Uint>(levels));
        SeedImmutableViewState(textureObject, static_cast<Uint>(levels));
    }

    // Shared front half of glTextureStorage2DMultisample/3DMultisample. The target forms are reached
    // through a binding and get their target validated there; by name the object itself has to be
    // checked, and so do the extents, which the binding path never sees (GL 4.6 core 8.19).
    static Bool ValidateNamedMultisampleStorage(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                                TextureTarget expectedTarget, GLsizei samples, GLsizei width,
                                                GLsizei height, GLsizei depth, const char* caller) {
        if (!textureObject) return false;
        if (textureObject->GetTarget() != expectedTarget) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", caller,
                    std::format("The effective target {} is not accepted by this entry point.",
                                MG_Util::ConvertTextureTargetToString(textureObject->GetTarget()))));
            return false;
        }
        if (width < 1 || height < 1 || depth < 1) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "width, height and depth must be positive."));
            return false;
        }
        const auto& limits = MG_Backend::pActiveBackendObject->GetDynamicParameters();
        if (width > limits.MaxTextureSize || height > limits.MaxTextureSize) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "width and height must not exceed GL_MAX_TEXTURE_SIZE."));
            return false;
        }
        if (depth > limits.MaxArrayTextureLayers) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "depth must not exceed GL_MAX_ARRAY_TEXTURE_LAYERS."));
            return false;
        }
        // More samples than the implementation offers is a request it cannot serve rather than a
        // malformed argument, so INVALID_OPERATION and not INVALID_VALUE. The limit comes from the
        // getter rather than the backend parameter it is derived from, because the frontend raises
        // that number - validating against the raw one would reject a count GL_MAX_SAMPLES
        // advertises.
        GLint maxSamples = 1;
        GetIntegerv(GL_MAX_SAMPLES, &maxSamples);
        if (samples > maxSamples) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller, "samples exceeds GL_MAX_SAMPLES."));
            return false;
        }
        // Storage is defined once. The mipmap forms go through ValidateTextureMutable for this; the
        // multisample ones reached the backend without ever asking.
        if (!ValidateTextureMutable(textureObject, caller)) return false;
        return true;
    }

    void TextureStorage2DMultisample(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width,
                                     GLsizei height, GLboolean fixedsamplelocations) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!ValidateNamedMultisampleStorage(textureObject, TextureTarget::Texture2DMultisample, samples, width, height,
                                             1, __func__))
            return;
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            TexStorage2DMultisample(target, samples, internalformat, width, height, fixedsamplelocations);
        });
    }

    void TextureStorage3DMultisample(GLuint texture, GLsizei samples, GLenum internalformat, GLsizei width,
                                     GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!ValidateNamedMultisampleStorage(textureObject, TextureTarget::Texture2DMultisampleArray, samples, width,
                                             height, depth, __func__))
            return;
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            TexStorage3DMultisample(target, samples, internalformat, width, height, depth, fixedsamplelocations);
        });
    }

    namespace {
        void RecordTextureViewError(ErrorCode code, const String& message) {
            MG_State::pGLContext->RecordError(code,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "TextureView", message));
        }

        // The internalformat the view-compatibility rule has to compare against, which is NOT
        // always ConvertTextureInternalFormatToGLEnum(GetFormat()): MobileGL answers every
        // compressed request with uncompressed storage and only remembers the requested enum on
        // the side, so a BPTC parent would otherwise present itself as RGBA8 and admit an RGBA8
        // view that table 8.21 forbids.
        GLenum ResolveTextureViewSourceFormat(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
            const auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(textureObject.get());
            if (mipmapTexture != nullptr && !textureObject->GetUploadTargets().empty()) {
                const TextureUploadTarget uploadTarget = textureObject->GetUploadTargets()[0];
                const GLenum stored = mipmapTexture->GetMipmapCompressedFormat(uploadTarget, 0);
                if (stored != GL_NONE) return stored;
                const GLenum requested = mipmapTexture->GetMipmapRequestedCompressedFormat(uploadTarget, 0);
                if (requested != GL_NONE) return requested;
            }
            return MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
        }

        Bool BackendSupportsTextureViews() {
            const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
            if (!activeBackendObject) return false;
            // Deliberately the ADVERTISED extension list rather than a separate capability bit:
            // it makes "MobileGL claims GL_ARB_texture_view" and "glTextureView actually works"
            // the same fact by construction. DirectVulkan always advertises it; DirectGLES only
            // does when the driver has EXT/OES_texture_view, because ES cannot otherwise give two
            // texture names one storage (see the no-EXT discussion in BackendObject_DirectGLES).
            const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions;
            return std::find(extensions.begin(), extensions.end(), E_GL_ARB_texture_view) != extensions.end();
        }
    } // namespace

    // glTextureView - ARB_texture_view, core since GL 4.3 (GL 4.6 core 8.18).
    //
    // Creates a texture whose STORAGE is another texture's, optionally reinterpreting the format
    // and narrowing the level/layer range. The error list below is the spec's, in the order the
    // conformance suite (KHR-GL43.texture_view.errors, cases a..s) walks it.
    void TextureView(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel,
                     GLuint numlevels, GLuint minlayer, GLuint numlayers) {
        if (!BackendSupportsTextureViews()) {
            // The honest answer when the backend cannot share one storage between two texture
            // names. Raising an error - and withholding the GL_ARB_texture_view string - is the
            // only alternative to a silent no-op that leaves the view with no storage at all,
            // which is indistinguishable from success at the call site and renders garbage.
            MGLOG_W_ONCE("glTextureView: the active backend has no texture-view support "
                         "(GL_EXT_texture_view / GL_OES_texture_view absent); raising GL_INVALID_OPERATION");
            RecordTextureViewError(ErrorCode::InvalidOperation,
                                   "The active backend does not support texture views.");
            return;
        }

        const TextureTarget viewTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        if (!TextureImpl::ValidateTextureTarget(viewTarget)) return;

        // a) <texture> is 0.
        if (texture == 0) {
            RecordTextureViewError(ErrorCode::InvalidValue, "texture must not be zero.");
            return;
        }
        // b) <texture> is not a name returned by glGenTextures.
        if (!MG_State::pGLContext->ValidateTextureName(texture)) {
            RecordTextureViewError(ErrorCode::InvalidOperation,
                                   std::format("texture {} is not a name returned by glGenTextures.", texture));
            return;
        }
        // c) <texture> has already been bound and given a target. A name that any bind (or
        // glCreateTextures, or an earlier glTextureView) has instantiated owns a texture object;
        // only a still-uninstantiated reservation may become a view.
        if (MG_State::pGLContext->ValidateTextureObject(texture)) {
            RecordTextureViewError(ErrorCode::InvalidOperation,
                                   std::format("texture {} has already been bound and given a target.", texture));
            return;
        }
        // d) <origtexture> is not the name of a texture object. Note the error code differs from
        // (b): INVALID_VALUE here, INVALID_OPERATION there.
        auto origTextureObject = MG_State::pGLContext->GetTextureObject(origtexture);
        if (origtexture == 0 || !origTextureObject) {
            RecordTextureViewError(ErrorCode::InvalidValue,
                                   std::format("origtexture {} is not the name of a texture object.", origtexture));
            return;
        }
        // e) <origtexture> is a mutable texture object. A view aliases storage that can never be
        // respecified underneath it, so only immutable storage qualifies.
        if (!origTextureObject->IsImmutable()) {
            RecordTextureViewError(ErrorCode::InvalidOperation,
                                   std::format("origtexture {} does not have immutable storage.", origtexture));
            return;
        }
        // f) target is incompatible with origtexture's target (table 8.20).
        const TextureTarget origTarget = origTextureObject->GetTarget();
        if (!TextureImpl::IsLegalTextureViewTargetPair(origTarget, viewTarget)) {
            RecordTextureViewError(
                ErrorCode::InvalidOperation,
                std::format("target {} is not a legal texture-view target for an origtexture whose target is {}.",
                            MG_Util::ConvertGLEnumToString(target),
                            MG_Util::ConvertGLEnumToString(MG_Util::ConvertTextureTargetToGLEnum(origTarget))));
            return;
        }
        // k)..q) the per-target <numlayers> constraints, all INVALID_VALUE.
        const Uint requiredLayers = TextureImpl::RequiredTextureViewLayerCount(viewTarget);
        if (requiredLayers != 0 && numlayers != requiredLayers) {
            RecordTextureViewError(ErrorCode::InvalidValue,
                                   std::format("target {} requires numlayers to be {}, but it is {}.",
                                               MG_Util::ConvertGLEnumToString(target), requiredLayers, numlayers));
            return;
        }
        if (viewTarget == TextureTarget::TextureCubeMapArray && (numlayers == 0 || numlayers % 6 != 0)) {
            RecordTextureViewError(
                ErrorCode::InvalidValue,
                std::format("GL_TEXTURE_CUBE_MAP_ARRAY requires numlayers to be a multiple of 6, but it is {}.",
                            numlayers));
            return;
        }
        // g)/h) the format-compatibility rule (table 8.21). A format WITH a view class may be
        // reinterpreted as any other format in the same class; a format with NO entry in the
        // table - every depth, stencil and depth/stencil format among them - may only ever be
        // viewed as itself, which is why the Better Clouds D24S8 view must name
        // GL_DEPTH24_STENCIL8 exactly.
        const GLenum origFormat = ResolveTextureViewSourceFormat(origTextureObject);
        const auto origViewClass = TextureImpl::GetTextureViewClass(origFormat);
        if (origViewClass == TextureImpl::TextureViewClass::None) {
            if (internalformat != origFormat) {
                RecordTextureViewError(
                    ErrorCode::InvalidOperation,
                    std::format("origtexture's internal format {} has no view class, so internalformat must be "
                                "identical to it, but it is {}.",
                                MG_Util::ConvertGLEnumToString(origFormat),
                                MG_Util::ConvertGLEnumToString(internalformat)));
                return;
            }
        } else if (TextureImpl::GetTextureViewClass(internalformat) != origViewClass) {
            RecordTextureViewError(
                ErrorCode::InvalidOperation,
                std::format("internalformat {} is not in the same view class as origtexture's internal format {}.",
                            MG_Util::ConvertGLEnumToString(internalformat),
                            MG_Util::ConvertGLEnumToString(origFormat)));
            return;
        }
        const TextureInternalFormat viewInternalFormat =
            MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        if (!TextureImpl::ValidateTextureInternalFormat(viewInternalFormat)) return;

        // i)/j) the range checks, both against the ORIGINAL's view state rather than its raw
        // level/layer counts. On a plain immutable texture TexStorage* seeded those with the full
        // extent, so the two agree; on a view-of-a-view they are what bounds the child to the
        // parent's already-narrowed window.
        const Uint origNumLevels = origTextureObject->GetViewNumLevels();
        const Uint origNumLayers = origTextureObject->GetViewNumLayers();
        if (minlevel >= origNumLevels) {
            RecordTextureViewError(ErrorCode::InvalidValue,
                                   std::format("minlevel {} is larger than origtexture's greatest level {}.", minlevel,
                                               origNumLevels == 0 ? 0 : origNumLevels - 1));
            return;
        }
        if (minlayer >= origNumLayers) {
            RecordTextureViewError(ErrorCode::InvalidValue,
                                   std::format("minlayer {} is larger than origtexture's greatest layer {}.", minlayer,
                                               origNumLayers == 0 ? 0 : origNumLayers - 1));
            return;
        }
        // r)/s) a cube-map or cube-map-array view demands square levels, because its faces are
        // square by definition and the storage it borrows is not reshaped.
        if (viewTarget == TextureTarget::TextureCubeMap || viewTarget == TextureTarget::TextureCubeMapArray) {
            const IntVec3 baseSize = origTextureObject->GetBaseSize();
            if (baseSize.x() != baseSize.y()) {
                RecordTextureViewError(
                    ErrorCode::InvalidOperation,
                    std::format("a cube-map texture view requires origtexture's width and height to match, but "
                                "they are {}x{}.",
                                baseSize.x(), baseSize.y()));
                return;
            }
        }

        // GL 4.6 core 8.18, verbatim:
        //   TEXTURE_VIEW_MIN_LEVEL  = <minlevel> + origtexture's TEXTURE_VIEW_MIN_LEVEL
        //   TEXTURE_VIEW_NUM_LEVELS = min(<numlevels>, origtexture's TEXTURE_VIEW_NUM_LEVELS - <minlevel>)
        //   TEXTURE_VIEW_MIN_LAYER  = <minlayer> + origtexture's TEXTURE_VIEW_MIN_LAYER
        //   TEXTURE_VIEW_NUM_LAYERS = min(<numlayers>, origtexture's TEXTURE_VIEW_NUM_LAYERS - <minlayer>)
        // Because the offsets ADD all the way down, the composed values are already expressed in
        // the ROOT's coordinates - which is exactly what lets the view point straight at the root
        // and skip the chain.
        const auto& storageOwner =
            origTextureObject->IsTextureView() ? origTextureObject->GetViewStorageOwner() : origTextureObject;
        const Uint composedMinLevel = minlevel + origTextureObject->GetViewMinLevel();
        const Uint composedNumLevels = std::min(numlevels, origNumLevels - minlevel);
        const Uint composedMinLayer = minlayer + origTextureObject->GetViewMinLayer();
        const Uint composedNumLayers = std::min(numlayers, origNumLayers - minlayer);

        const auto& viewObject = MG_State::pGLContext->CreateTextureViewObject(
            texture, viewTarget, storageOwner, composedMinLevel, composedNumLevels, composedMinLayer,
            composedNumLayers);
        if (!viewObject) {
            RecordTextureViewError(ErrorCode::InvalidOperation, "Failed to create the texture view object.");
            return;
        }
        viewObject->SetInternalFormat(viewInternalFormat);
        viewObject->SetSamples(storageOwner->GetSamples());
        viewObject->SetFixedSampleLocations(storageOwner->HasFixedSampleLocations());
    }

    void TexStorage1D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width) {
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
        // Not ValidateTextureUploadTarget: GL_TEXTURE_CUBE_MAP is a legal glTexStorage2D target but
        // has no single upload target - it allocates all six faces - so validating one would reject
        // it. The accepted set for this entry point is the dimension's storage targets, and the
        // by-name form below does the per-face work.
        if (!IsTextureStorageTargetForDimension(textureTarget, 1)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             std::format("Target {} does not take 1D immutable storage.",
                                                         MG_Util::ConvertGLEnumToString(target))));
            return;
        }

        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        auto& textureObject = bindingSlot.GetBoundObject();
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;

        TextureStorage1D(textureObject->GetExternalIndex(), levels, internalformat, width);
    }

    void TexStorage2D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height) {
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
        // Not ValidateTextureUploadTarget: GL_TEXTURE_CUBE_MAP is a legal glTexStorage2D target but
        // has no single upload target - it allocates all six faces - so validating one would reject
        // it. The accepted set for this entry point is the dimension's storage targets, and the
        // by-name form below does the per-face work.
        if (!IsTextureStorageTargetForDimension(textureTarget, 2)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             std::format("Target {} does not take 2D immutable storage.",
                                                         MG_Util::ConvertGLEnumToString(target))));
            return;
        }

        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        auto& textureObject = bindingSlot.GetBoundObject();
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;

        TextureStorage2D(textureObject->GetExternalIndex(), levels, internalformat, width, height);
    }

    void TexStorage3D(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height,
                      GLsizei depth) {
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        if (!TextureImpl::ValidateTextureTarget(textureTarget)) return;
        // Not ValidateTextureUploadTarget: GL_TEXTURE_CUBE_MAP is a legal glTexStorage2D target but
        // has no single upload target - it allocates all six faces - so validating one would reject
        // it. The accepted set for this entry point is the dimension's storage targets, and the
        // by-name form below does the per-face work.
        if (!IsTextureStorageTargetForDimension(textureTarget, 3)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             std::format("Target {} does not take 3D immutable storage.",
                                                         MG_Util::ConvertGLEnumToString(target))));
            return;
        }

        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget);
        auto& textureObject = bindingSlot.GetBoundObject();
        if (!TextureImpl::ValidateTextureObject(textureObject)) return;
        if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return;

        TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth);
    }

    // Unlike glTexImage*Multisample, where a zero-sized image is a legal deallocation (see
    // AllocateMultisampleTextureStorage), the immutable forms take a strictly positive size: GL
    // 4.6 core 8.19 makes width, height or depth < 1 INVALID_VALUE. Without this the shared
    // _State helper would deallocate the image and TexStorageMultisample_State would then freeze
    // the now-imageless texture as immutable.
    static Bool ValidateTexStorageMultisampleSize(GLsizei width, GLsizei height, GLsizei depth, const char* caller) {
        if (width >= 1 && height >= 1 && depth >= 1) {
            return true;
        }
        MG_State::pGLContext->RecordError(
            ErrorCode::InvalidValue,
            MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                         "Immutable multisample storage requires width, height and depth >= 1."));
        return false;
    }

    // The multisample storage forms allocate exactly what the glTexImage*Multisample ones do, and
    // then freeze it: TEXTURE_IMMUTABLE_FORMAT becomes TRUE and a second call is INVALID_OPERATION
    // (GL 4.6 core 8.19). Only the allocation was shared before, so a multisample texture stayed
    // mutable forever and could be respecified any number of times.
    static void TexStorageMultisample_State(GLenum target, Bool allocated, const char* caller) {
        static_cast<void>(caller);
        if (!allocated) return;
        const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        if (TextureImpl::IsProxyTextureTarget(MG_Util::ConvertGLEnumToTextureUploadTarget(target))) return;
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
        if (!textureObject) return;
        textureObject->SetImmutableLevels(1);
        SeedImmutableViewState(textureObject, 1);
    }

    void TexStorage2DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
                                 GLsizei height, GLboolean fixedsamplelocations) {
        const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
        if (!ValidateTexStorageMultisampleSize(width, height, 1, __func__)) return;
        TexStorageMultisample_State(
            target, TexImage2DMultisample_State(target, samples, internalformat, width, height, fixedsamplelocations),
            __func__);
    }

    void TexStorage3DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width,
                                 GLsizei height, GLsizei depth, GLboolean fixedsamplelocations) {
        const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        if (!ValidateTextureMutable(activeUnit.GetBindingSlot(textureTarget).GetBoundObject(), __func__)) return;
        if (!ValidateTexStorageMultisampleSize(width, height, depth, __func__)) return;
        TexStorageMultisample_State(target,
                                    TexImage3DMultisample_State(target, samples, internalformat, width, height, depth,
                                                                fixedsamplelocations),
                                    __func__);
    }

    void TextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type,
                           const void* pixels) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            TexSubImage1D_State(target, level, xoffset, width, format, type, pixels);
        });
    }

    void TextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height,
                           GLenum format, GLenum type, const void* pixels) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;

        TextureInputFormat textureInputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
        TexturePixelDataType texturePixelDataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
        if (!TextureImpl::ValidateTexturePixelDataType(texturePixelDataType)) return;
        if (!TextureImpl::ValidateTextureInputFormat(textureInputFormat)) return;
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;
        if (!TextureImpl::ValidateTextureSizeRange(width, height, 1)) return;
        if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
            return;
        }

        auto textureUploadTarget = GetPrimaryUploadTarget(textureObject);
        if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
        if (!TextureImpl::ValidateTextureInternalFormatCompatibleWithInput(textureInputFormat, textureObject->GetFormat(),
                                                                           texturePixelDataType))
            return;

        auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        if (static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture level is out of range."));
            return;
        }
        if (!TextureImpl::ValidateTextureSubImageOffsets(textureObject, xoffset, width, yoffset, height)) return;
        // This entry point does not go through TexSubImage2D_State, so it needs the unpack-buffer
        // rules of its own.
        if (!ValidatePixelUnpackBufferSource(pixels, textureInputFormat, texturePixelDataType, {width, height, 1},
                                             __func__))
            return;

        const void* originalPixels = pixels;
        const auto& pixelUnpackBufferObject =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
        if (pixelUnpackBufferObject) {
            originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
                             reinterpret_cast<SizeT>(pixels);
        }
        if (!originalPixels) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "No data supplied from pixels parameter and no PBO bound."));
            return;
        }

        SizeT inputSize = 0;
        void* processedPixels = MG_Util::PixelStoreProcessor::ProcessTexturePixelsDataUnpack(
            originalPixels, MG_State::pGLContext->GetPixelStoreParameters(true), textureObject->GetFormat(),
            textureInputFormat, texturePixelDataType, {width, height, 1}, false, inputSize);
        if (!processedPixels || inputSize == 0) {
            if (processedPixels) free(processedPixels);
            return;
        }

        const auto texelSize = textureMipmapObject->GetMipmapTexelSize(textureUploadTarget, level);
        const SizeT internalBpp = MG_Util::GetInternalBytesPerPixel(textureObject->GetFormat(), texturePixelDataType);
        const SizeT srcRowSize = static_cast<SizeT>(width) * internalBpp;
        const SizeT destRowSize = static_cast<SizeT>(texelSize.x()) * internalBpp;

        const auto* srcData = static_cast<const Uint8*>(processedPixels);
        Uint8* destData = static_cast<Uint8*>(textureMipmapObject->MapMipmapData(textureUploadTarget, level));
        if (destData) {
            for (GLsizei y = 0; y < height; ++y) {
                const SizeT destRowOffset = static_cast<SizeT>(yoffset + y) * destRowSize +
                                            static_cast<SizeT>(xoffset) * internalBpp;
                const SizeT srcRowOffset = static_cast<SizeT>(y) * srcRowSize;
                Memcpy(destData + destRowOffset, srcData + srcRowOffset, srcRowSize);
            }
            textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, true);
        }
        free(processedPixels);
    }

    void CompressedTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLsizei width, GLenum format,
                                     GLsizei imageSize, const void* data) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            CompressedTexSubImage1D_State(target, level, xoffset, width, format, imageSize, data);
        });
    }

    void CompressedTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
                                     GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            CompressedTexSubImage2D_State(target, level, xoffset, yoffset, width, height, format, imageSize, data);
        });
    }

    void CompressedTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
                                     GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
                                     const void* data) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            CompressedTexSubImage3D_State(target, level, xoffset, yoffset, zoffset, width, height, depth, format,
                                          imageSize, data);
        });
    }

    void TextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
                           GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            TexSubImage3D_State(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels);
        });
    }

    void TextureParameteri(GLuint texture, GLenum pname, GLint param) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!ValidateNamedTextureParameterTarget(textureObject, __func__)) return;
        TextureParameterObject_State(textureObject, pname, param, __func__);
    }

    void TextureParameterf(GLuint texture, GLenum pname, GLfloat param) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!ValidateNamedTextureParameterTarget(textureObject, __func__)) return;
        TextureParameterObjectf_State(textureObject, pname, param, __func__);
    }

    void TextureParameterfv(GLuint texture, GLenum pname, const GLfloat* params) {
        if (!params) return;
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { TexParameterfv_State(target, pname, params); });
    }

    void TextureParameteriv(GLuint texture, GLenum pname, const GLint* params) {
        if (!params) return;
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { TexParameteriv_State(target, pname, params); });
    }

    void TextureParameterIiv(GLuint texture, GLenum pname, const GLint* params) {
        if (!params) return;
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            TexParameterIiv_State(target, pname, params);
        });
    }

    void TextureParameterIuiv(GLuint texture, GLenum pname, const GLuint* params) {
        if (!params) return;
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            TexParameterIuiv_State(target, pname, params);
        });
    }

    void BindTextureUnit(GLuint unit, GLuint texture) {
        if (unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture unit is out of range."));
            return;
        }

        auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast<Int>(unit));
        if (texture == 0) {
            // GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each
            // target's default texture object (the unit's initial state).
            Bool changed = false;
            for (auto& slot : textureUnit.GetAllBindingSlots()) {
                if (slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()))) changed = true;
            }
            MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
            return;
        }

        auto& textureObject = MG_State::pGLContext->GetTextureObject(texture);
        if (!textureObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture object does not exist."));
            return;
        }
        const Bool changed = textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject);
        MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit), changed);
    }

    GLint GetCombinedTextureImageUnitCount() {
        GLint maxTextureUnits = 0;
        GetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
        return std::min<GLint>(std::max(maxTextureUnits, 0), MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS);
    }

    namespace {
        // ARB_multi_bind checks the whole [first, first + count) range before binding anything and
        // reports an overrun as INVALID_OPERATION - not the INVALID_VALUE the single-bind entry
        // points report for an out-of-range unit, and not after binding the in-range prefix.
        Bool ValidateMultiBindUnitRange(GLuint first, GLsizei count, GLint unitCount, const char* funcName) {
            if (count < 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName, "count must be non-negative."));
                return false;
            }
            if (static_cast<Uint64>(first) + static_cast<Uint64>(count) > static_cast<Uint64>(unitCount)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
                                                 std::format("first + count ({} + {}) exceeds the {} available units.",
                                                             first, count, unitCount)));
                return false;
            }
            return true;
        }

        // ARB_multi_bind states the equivalence to a loop of single binds "except that <textures>
        // will not be created if they do not exist": glBindTexture instantiates a name GenTextures
        // merely reserved, the multi-bind entry points must refuse it. The error class is
        // INVALID_OPERATION for both of them, where the scalar glBindImageTexture reports
        // INVALID_VALUE - hence the check here rather than inside BindImageTexture.
        //
        // Deliberately PER ELEMENT: the extension defines these calls as a loop, so a bad entry
        // costs its own unit and leaves the rest of the range bound.
        SharedPtr<MG_State::GLState::ITextureObject> ResolveMultiBindTexture(GLuint texture, GLsizei index,
                                                                            const char* funcName) {
            SharedPtr<MG_State::GLState::ITextureObject> textureObject =
                MG_State::pGLContext->GetTextureObject(texture);
            if (!textureObject) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", funcName,
                        std::format("textures[{}] ({}) is not the name of an existing texture object.", index,
                                    texture)));
            }
            return textureObject;
        }

        // ARB_multi_bind: an element naming texture zero unbinds EVERY target of its unit, i.e.
        // rebinds each target's default texture object - the unit's initial state. Same rule
        // glBindTextureUnit(unit, 0) follows.
        void UnbindAllTargetsOnUnit(Int unit) {
            auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
            Bool changed = false;
            for (auto& slot : textureUnit.GetAllBindingSlots()) {
                if (slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget()))) changed = true;
            }
            MG_State::pGLContext->NoteTextureUnitTouched(unit, changed);
        }
    } // namespace

    // ARB_multi_bind: glBindTextures binds each texture to ITS OWN target on unit <first> + i, so
    // there is no target parameter and no way to express it through glBindTexture - the per-unit,
    // by-object form glBindTextureUnit uses is the one that matches. A NULL <textures> unbinds the
    // whole range.
    void BindTextures(GLuint first, GLsizei count, const GLuint* textures) {
        if (!ValidateMultiBindUnitRange(first, count, GetCombinedTextureImageUnitCount(), __func__)) return;

        for (GLsizei i = 0; i < count; ++i) {
            const GLuint texture = textures ? textures[i] : 0;
            const Int unit = static_cast<Int>(first) + i;
            if (texture == 0) {
                UnbindAllTargetsOnUnit(unit);
                continue;
            }
            const SharedPtr<MG_State::GLState::ITextureObject> textureObject =
                ResolveMultiBindTexture(texture, i, __func__);
            if (!textureObject) continue;

            auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
            const Bool changed = textureUnit.GetBindingSlot(textureObject->GetTarget()).Bind(textureObject);
            MG_State::pGLContext->NoteTextureUnitTouched(unit, changed);
        }
    }

    // ARB_multi_bind: glBindImageTextures is a loop of glBindImageTexture with every parameter but
    // the unit and the texture fixed by the spec - level 0, layered, layer 0, READ_WRITE, and the
    // texture's own internal format. An element that names texture zero resets the unit.
    void BindImageTextures(GLuint first, GLsizei count, const GLuint* textures) {
        if (!ValidateMultiBindUnitRange(first, count, static_cast<GLint>(GetAdvertisedImageUnitCount()), __func__)) {
            return;
        }

        for (GLsizei i = 0; i < count; ++i) {
            const GLuint texture = textures ? textures[i] : 0;
            const GLuint unit = first + static_cast<GLuint>(i);
            if (texture == 0) {
                BindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8);
                continue;
            }
            const SharedPtr<MG_State::GLState::ITextureObject> textureObject =
                ResolveMultiBindTexture(texture, i, __func__);
            if (!textureObject) continue;

            // "An INVALID_OPERATION error is generated if the internal format of any texture is not
            // supported for image textures" - a texture that has never been given storage has no
            // format at all and lands here too, rather than being reported as a bad enum by the
            // scalar path.
            const GLenum format = MG_Util::ConvertTextureInternalFormatToGLEnum(textureObject->GetFormat());
            if (!IsValidImageTextureFormat(format)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", __func__,
                        std::format("textures[{}] ({}) has an internal format that is not supported for image "
                                    "textures.",
                                    i, texture)));
                continue;
            }
            BindImageTexture(unit, texture, 0, GL_TRUE, 0, GL_READ_WRITE, format);
        }
    }

    // The half glGetTextureImage and glGetTextureSubImage share: which of the two readbacks answers,
    // for ONE named upload target. Factored out so the sub-image form can name a cube FACE - the
    // by-name spelling of the face token glGetTexImage takes - instead of re-deriving the target and
    // silently landing on the +X face the way the delegation it replaces did.
    static void GetTextureImageForUploadTarget(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                               TextureUploadTarget uploadTarget, GLint level, GLenum format,
                                               GLenum type, GLsizei bufSize, void* pixels, const char* caller) {
        if (MG_Backend::pActiveBackendObject != nullptr &&
            MG_Backend::pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan &&
            MG_Backend::gBackendFunctionsTable.GL.GetTextureImage != nullptr) {
            MG_Backend::gBackendFunctionsTable.GL.GetTextureImage(textureObject, uploadTarget, level, format, type,
                                                                  bufSize, pixels);
            return;
        }
        CopyTextureImageToClientOrPBO_State(textureObject, uploadTarget, level, format, type, bufSize, pixels, caller);
    }

    void GetTextureImage(GLuint texture, GLint level, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;
        if (!ValidateTextureImageQuery(textureObject, level, MG_Util::ConvertGLEnumToTextureInputFormat(format),
                                       MG_Util::ConvertGLEnumToTexturePixelDataType(type), bufSize, pixels,
                                       __func__)) {
            return;
        }
        GetTextureImageForUploadTarget(textureObject, GetPrimaryUploadTarget(textureObject), level, format, type,
                                       bufSize, pixels, __func__);
    }

    void GetCompressedTextureImage(GLuint texture, GLint level, GLsizei bufSize, void* pixels) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;
        // Level first: GL 4.6 core 8.11 wants INVALID_VALUE for an out-of-range level even when the
        // texture would also fail the compressed check below.
        if (!TextureImpl::ValidateTextureLevelNumber(level)) return;

        // Unlike glGetTextureImage this never asks a backend: the compressed image only ever exists
        // in the CPU shadow (no backend was handed the compressed bytes at all), so the shadow is
        // authoritative rather than potentially stale.
        CopyCompressedTextureImageToClientOrPBO(textureObject, GetPrimaryUploadTarget(textureObject), level, bufSize,
                                                pixels, __func__);
    }

    void GetTextureSubImage(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
                            GLsizei height, GLsizei depth, GLenum format, GLenum type, GLsizei bufSize, void* pixels) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;
        if (level < 0 || xoffset < 0 || yoffset < 0 || zoffset < 0 || width < 0 || height < 0 || depth < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture sub-image range is invalid."));
            return;
        }
        if (textureObject->GetStorageType() != TextureStorageType::Mipmap) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture storage is not mipmap-backed."));
            return;
        }

        const auto uploadTarget = GetPrimaryUploadTarget(textureObject);
        auto* textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        if (static_cast<Uint>(level) >= textureMipmapObject->GetMipmapLevelCount()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture level is out of range."));
            return;
        }

        const auto texelSize = textureMipmapObject->GetMipmapTexelSize(uploadTarget, static_cast<Uint>(level));
        // On a cube map, z is the FACE axis. A cube map's level is stored per face, so its level
        // size reads z = 1 whichever face named it - but GL 4.6 core 8.11.4 addresses the six faces
        // of a cube map through zoffset/depth, exactly the six layers a face token names for
        // glGetTexImage. Without this arm the z range was measured against that 1 and only zoffset 0
        // (the +X face) was expressible; the other five were rejected as a partial read.
        //
        // Only ONE face at a time. depth > 1 would have to concatenate faces into the destination,
        // which is the same unimplemented multi-image packing the check below still refuses.
        const Bool isSingleCubeFaceRead = textureObject->GetTarget() == TextureTarget::TextureCubeMap &&
                                          depth == 1 && zoffset < 6;
        const Bool isFullLevelRead = xoffset == 0 && yoffset == 0 && width == texelSize.x() &&
                                     height == texelSize.y() &&
                                     (isSingleCubeFaceRead || (zoffset == 0 && depth == texelSize.z()));
        if (!isFullLevelRead) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Partial texture sub-image readback is not implemented yet."));
            return;
        }

        const TextureUploadTarget readUploadTarget =
            isSingleCubeFaceRead ? static_cast<TextureUploadTarget>(
                                       static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + zoffset)
                                 : uploadTarget;
        if (!ValidateTextureImageQuery(textureObject, level, MG_Util::ConvertGLEnumToTextureInputFormat(format),
                                       MG_Util::ConvertGLEnumToTexturePixelDataType(type), bufSize, pixels, __func__,
                                       isSingleCubeFaceRead ? 1u : 0u)) {
            return;
        }
        GetTextureImageForUploadTarget(textureObject, readUploadTarget, level, format, type, bufSize, pixels,
                                       __func__);
    }

    // A buffer texture carries none of the sampler or level state these queries report. Reached by
    // name there is no target token to blame, so the wrong object is INVALID_OPERATION rather than
    // the INVALID_ENUM the target forms report for an unaccepted target (GL 4.6 core 8.11).
    static Bool ValidateNamedTextureHasParameters(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                                  const char* caller) {
        if (!textureObject) return false;
        if (textureObject->GetStorageType() == TextureStorageType::Buffer) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "The effective target of `texture` has no texture parameters."));
            return false;
        }
        return true;
    }

    void GetTextureParameteriv(GLuint texture, GLenum pname, GLint* params) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameteriv_State(target, pname, params); });
    }

    void GetTextureParameterfv(GLuint texture, GLenum pname, GLfloat* params) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterfv_State(target, pname, params); });
    }

    void GetTextureParameterIiv(GLuint texture, GLenum pname, GLint* params) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterIiv_State(target, pname, params); });
    }

    void GetTextureParameterIuiv(GLuint texture, GLenum pname, GLuint* params) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!ValidateNamedTextureHasParameters(textureObject, __func__)) return;
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) { GetTexParameterIuiv_State(target, pname, params); });
    }

    void GetTextureLevelParameteriv(GLuint texture, GLint level, GLenum pname, GLint* params) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            GetTexLevelParameteriv_State(target, level, pname, params);
        });
    }

    void GetTextureLevelParameterfv(GLuint texture, GLint level, GLenum pname, GLfloat* params) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            GetTexLevelParameterfv_State(target, level, pname, params);
        });
    }

    void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
                          GLenum format) {
        if (unit >= GetAdvertisedImageUnitCount()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Image texture unit is out of range."));
            return;
        }
        if (level < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture level must be non-negative."));
            return;
        }
        if (layer < 0 && layered == GL_FALSE) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture layer must be non-negative."));
            return;
        }
        if (access != GL_READ_ONLY && access != GL_WRITE_ONLY && access != GL_READ_WRITE) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Invalid image texture access."));
            return;
        }
        if (!IsValidImageTextureFormat(format)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Invalid image texture format."));
            return;
        }

        SharedPtr<MG_State::GLState::ITextureObject> textureObject;
        if (texture != 0) {
            if (!MG_State::pGLContext->ValidateTextureObject(texture)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Texture name is not a texture object."));
                return;
            }
            textureObject = MG_State::pGLContext->GetTextureObject(texture);
        }

        auto bindImageTexture = MG_Backend::gBackendFunctionsTable.GL.BindImageTexture;
        if (!bindImageTexture) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Backend does not support image texture binding."));
            return;
        }

        MG_State::pGLContext->GetImageTextureBinding(static_cast<Int>(unit))
            .Bind(textureObject, level, layered, layer, access, format);
        MG_State::pGLContext->NoteTextureUnitTouched(static_cast<Int>(unit));
        bindImageTexture(unit, texture, level, layered, layer, access, format);
    }

    // GL 4.6 core 8.14.4: a cube map that is not cube complete has no consistent set of faces to
    // filter down, so generating its mipmaps is INVALID_OPERATION. Without this the incomplete
    // texture reached the backend, where DirectVulkan asserts on it and takes the process down.
    Bool ValidateGenerateMipmapTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
                                       const char* caller) {
        if (!textureObject) return false;
        const auto target = textureObject->GetTarget();
        if ((target == TextureTarget::TextureCubeMap || target == TextureTarget::TextureCubeMapArray) &&
            !textureObject->IsComplete()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
                                             "Mipmap generation requires a cube complete cube map texture."));
            return false;
        }
        return true;
    }

    void GenerateMipmap(GLenum target) {
        const auto textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        if (!TextureImpl::ValidateTextureTarget(textureTarget)) {
            return;
        }
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        auto& textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
        if (!textureObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "GenerateMipmap requires a bound texture."));
            return;
        }
        if (!ValidateGenerateMipmapTexture(textureObject, __func__)) return;

        auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires mipmap texture storage.");
        EnsureGeneratedMipmapStorageAllocated(*mipmapTexture);
        GenerateMipmap_Backend(target);
    }

    void GenerateTextureMipmap(GLuint texture) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!ValidateGenerateMipmapTexture(textureObject, __func__)) return;
        auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
        MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateTextureMipmap requires mipmap texture storage.");
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum target) {
            EnsureGeneratedMipmapStorageAllocated(*mipmapTexture);
            GenerateMipmap_Backend(target);
        });
    }

    void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
        if (!GetTexImage_State(target, level, format, type, pixels)) return;
        if (MG_Backend::gBackendFunctionsTable.GL.GetTexImage != nullptr) {
            GetTexImage_Backend(target, level, format, type, pixels);
            return;
        }
        TextureUploadTarget textureUploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        const auto& textureObject = activeUnit.GetBindingSlot(textureTarget).GetBoundObject();
        CopyTextureImageToClientOrPBO_State(textureObject, textureUploadTarget, level, format, type, -1, pixels,
                                            __func__);
    }

    void GetInternalformativ(GLenum target, GLenum internalformat, GLenum pname, GLsizei bufSize, GLint* params) {
        if (!params || bufSize <= 0) return;

        const TextureTarget textureTarget = MG_Util::ConvertGLEnumToTextureTarget(target);
        const Bool isRenderbufferTarget = target == GL_RENDERBUFFER;
        if (!isRenderbufferTarget && !TextureImpl::ValidateTextureTarget(textureTarget)) return;

        TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
        textureInternalFormat = MG_Util::ConvertInternalFormatToSized(textureInternalFormat, TextureInputFormat::RGBA,
                                                                      TexturePixelDataType::UnsignedByte);
        if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;

        GLenum preferredInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat);
        GLenum imageFormat = GL_RGBA;
        GLenum imageType = GL_UNSIGNED_BYTE;
        MG_Util::TextureFormatProcessor::NormalizePixelFormat(preferredInternalFormat, PixelFormatNormalizeOptionBit::None,
                                                              &preferredInternalFormat, &imageFormat, &imageType);

        auto writeValues = [&](std::initializer_list<GLint> values) {
            GLsizei index = 0;
            for (GLint value : values) {
                if (index >= bufSize) break;
                params[index++] = value;
            }
            while (index < bufSize) {
                params[index++] = 0;
            }
        };

        const Bool isDepthFormat = MG_Util::IsDepthFormatInternalFormat(textureInternalFormat);
        const Bool isStencilFormat = MG_Util::IsStencilFormatInternalFormat(textureInternalFormat);
        const ComponentSizes componentSizes = MG_Util::GetComponentSizesForInternalFormat(textureInternalFormat);
        const Bool isIntegerFormat = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
                                     imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;

        SizeT targetIndex = isRenderbufferTarget ? MG_Backend::GetRenderbufferFormatCapabilityTargetIndex()
                                                 : MG_Backend::GetFormatCapabilityTargetIndex(textureTarget);
        if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount) return;
        const SizeT formatIndex = static_cast<SizeT>(textureInternalFormat);

        MG_Backend::FormatCapabilityFlags fullCaps{};
        MG_Backend::FormatCapabilityFlags caveatCaps{};
        const Vector<Int>* sampleCounts = nullptr;
        if (MG_Backend::pActiveBackendObject) {
            const auto& cache = MG_Backend::pActiveBackendObject->GetFormatCapabilities();
            fullCaps = cache.FullCaps[targetIndex][formatIndex];
            caveatCaps = cache.CaveatCaps[targetIndex][formatIndex];
            sampleCounts = &cache.SampleCounts[targetIndex][formatIndex];
        }

        auto hasFull = [&](MG_Backend::FormatCapability capability) {
            return MG_Backend::HasFormatCapability(fullCaps, capability);
        };
        auto hasCaveat = [&](MG_Backend::FormatCapability capability) {
            return MG_Backend::HasFormatCapability(caveatCaps, capability);
        };
        auto supportFor = [&](MG_Backend::FormatCapability capability) -> GLint {
            if (hasFull(capability)) return GL_FULL_SUPPORT;
            if (hasCaveat(capability)) return GL_CAVEAT_SUPPORT;
            return GL_NONE;
        };
        auto supportForWithFallback = [&](MG_Backend::FormatCapability primary,
                                          MG_Backend::FormatCapability fallback) -> GLint {
            if (hasFull(primary)) return GL_FULL_SUPPORT;
            if (hasCaveat(primary) || hasFull(fallback) || hasCaveat(fallback)) return GL_CAVEAT_SUPPORT;
            return GL_NONE;
        };
        switch (pname) {
        case GL_INTERNALFORMAT_SUPPORTED:
            writeValues({(hasFull(MG_Backend::FormatCapability::Creatable) ||
                          hasCaveat(MG_Backend::FormatCapability::Creatable))
                             ? GL_TRUE
                             : GL_FALSE});
            return;
        case GL_INTERNALFORMAT_PREFERRED:
            writeValues({static_cast<GLint>(preferredInternalFormat)});
            return;
        case GL_INTERNALFORMAT_RED_SIZE:
            writeValues({componentSizes.Red});
            return;
        case GL_INTERNALFORMAT_GREEN_SIZE:
            writeValues({componentSizes.Green});
            return;
        case GL_INTERNALFORMAT_BLUE_SIZE:
            writeValues({componentSizes.Blue});
            return;
        case GL_INTERNALFORMAT_ALPHA_SIZE:
            writeValues({componentSizes.Alpha});
            return;
        case GL_INTERNALFORMAT_DEPTH_SIZE:
            writeValues({componentSizes.Depth});
            return;
        case GL_INTERNALFORMAT_STENCIL_SIZE:
            writeValues({componentSizes.Stencil});
            return;
        case GL_INTERNALFORMAT_SHARED_SIZE:
            writeValues({textureInternalFormat == TextureInternalFormat::RGB9E5 ? 5 : 0});
            return;
        case GL_INTERNALFORMAT_RED_TYPE:
            writeValues({GetTextureComponentType(textureInternalFormat, componentSizes.Red, false, false)});
            return;
        case GL_INTERNALFORMAT_GREEN_TYPE:
            writeValues({GetTextureComponentType(textureInternalFormat, componentSizes.Green, false, false)});
            return;
        case GL_INTERNALFORMAT_BLUE_TYPE:
            writeValues({GetTextureComponentType(textureInternalFormat, componentSizes.Blue, false, false)});
            return;
        case GL_INTERNALFORMAT_ALPHA_TYPE:
            writeValues({GetTextureComponentType(textureInternalFormat, componentSizes.Alpha, false, false)});
            return;
        case GL_INTERNALFORMAT_DEPTH_TYPE:
            writeValues({GetTextureComponentType(textureInternalFormat, componentSizes.Depth, true, false)});
            return;
        case GL_INTERNALFORMAT_STENCIL_TYPE:
            writeValues({GetTextureComponentType(textureInternalFormat, componentSizes.Stencil, false, true)});
            return;
        case GL_TEXTURE_IMAGE_FORMAT:
            writeValues({static_cast<GLint>(imageFormat)});
            return;
        case GL_TEXTURE_IMAGE_TYPE:
            writeValues({static_cast<GLint>(imageType)});
            return;
        case GL_TEXTURE_COMPRESSED:
        case GL_TEXTURE_COMPRESSED_BLOCK_WIDTH:
        case GL_TEXTURE_COMPRESSED_BLOCK_HEIGHT:
        case GL_TEXTURE_COMPRESSED_BLOCK_SIZE:
            writeValues({0});
            return;
        case GL_COLOR_COMPONENTS:
            writeValues({(!isDepthFormat && !isStencilFormat) ? GL_TRUE : GL_FALSE});
            return;
        case GL_DEPTH_COMPONENTS:
            writeValues({isDepthFormat ? GL_TRUE : GL_FALSE});
            return;
        case GL_STENCIL_COMPONENTS:
            writeValues({isStencilFormat ? GL_TRUE : GL_FALSE});
            return;
        case GL_FRAMEBUFFER_RENDERABLE:
            writeValues({supportFor(MG_Backend::FormatCapability::FramebufferRenderable)});
            return;
        case GL_FRAMEBUFFER_RENDERABLE_LAYERED:
            writeValues({supportFor(MG_Backend::FormatCapability::FramebufferLayered)});
            return;
        case GL_FILTER:
            writeValues({supportForWithFallback(MG_Backend::FormatCapability::LinearFilter,
                                                MG_Backend::FormatCapability::Sampled)});
            return;
        case GL_MIPMAP:
            writeValues({supportForWithFallback(MG_Backend::FormatCapability::GenerateMipmap,
                                                MG_Backend::FormatCapability::Sampled)});
            return;
        case GL_TEXTURE_GATHER:
        case GL_TEXTURE_GATHER_SHADOW:
            writeValues({supportFor(MG_Backend::FormatCapability::TextureGather)});
            return;
        case GL_TEXTURE_SHADOW:
            writeValues({supportFor(MG_Backend::FormatCapability::TextureShadow)});
            return;
        case GL_NUM_SAMPLE_COUNTS:
            writeValues({sampleCounts != nullptr ? static_cast<GLint>(sampleCounts->size()) : 0});
            return;
        case GL_SAMPLES:
            if (sampleCounts != nullptr) {
                GLsizei index = 0;
                for (Int sampleCount : *sampleCounts) {
                    if (index >= bufSize) break;
                    params[index++] = sampleCount;
                }
                while (index < bufSize) {
                    params[index++] = 0;
                }
                return;
            }
            writeValues({});
            return;
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "pname is not supported by GetInternalformativ."));
            return;
        }
    }

    void GetMultisamplefv(GLenum pname, GLuint index, GLfloat* val) {
        if (val == nullptr) return;
        if (pname != GL_SAMPLE_POSITION) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Only GL_SAMPLE_POSITION is supported."));
            return;
        }

        const Int maxSamples = MG_Backend::pActiveBackendObject != nullptr
            ? std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1)
            : 1;
        if (static_cast<Int>(index) >= maxSamples) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Sample index is out of range."));
            return;
        }

        // Keep sample positions deterministic even before the backend exposes vendor-specific patterns.
        val[0] = 0.5f;
        val[1] = 0.5f;
    }

    void TexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
                       GLsizei height, GLsizei depth, GLenum format, GLenum type, const void* pixels) {
        TexSubImage3D_State(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels);
    }

    void TexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height,
                       GLenum format, GLenum type, const void* pixels) {
        TexSubImage2D_State(target, level, xoffset, yoffset, width, height, format, type, pixels);
    }

    void TexSubImage1D(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type,
                       const GLvoid* pixels) {
        TexSubImage1D_State(target, level, xoffset, width, format, type, pixels);
    }

    void TexParameterf(GLenum target, GLenum pname, GLfloat param) {
        TexParameterf_State(target, pname, param);
    }

    void TexParameteri(GLenum target, GLenum pname, GLint param) {
        TexParameteri_State(target, pname, param);
    }

    void TexParameterfv(GLenum target, GLenum pname, const GLfloat* params) {
        TexParameterfv_State(target, pname, params);
    }

    void TexParameteriv(GLenum target, GLenum pname, const GLint* params) {
        TexParameteriv_State(target, pname, params);
    }

    void TexParameterIiv(GLenum target, GLenum pname, const GLint* params) {
        TexParameterIiv_State(target, pname, params);
    }

    void TexParameterIuiv(GLenum target, GLenum pname, const GLuint* params) {
        TexParameterIuiv_State(target, pname, params);
    }

    void TexImage3DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height,
                               GLsizei depth, GLboolean fixedsamplelocations) {
        TexImage3DMultisample_State(target, samples, internalformat, width, height, depth, fixedsamplelocations);
    }

    void TexImage2DMultisample(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height,
                               GLboolean fixedsamplelocations) {
        TexImage2DMultisample_State(target, samples, internalformat, width, height, fixedsamplelocations);
    }

    void TexImage3D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth,
                    GLint border, GLenum format, GLenum type, const void* pixels) {
        TexImage3D_State(target, level, internalformat, width, height, depth, border, format, type, pixels);
    }

    void TexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border,
                    GLenum format, GLenum type, const void* pixels) {
        TexImage2D_State(target, level, internalformat, width, height, border, format, type, pixels);
    }

    void TexImage1D(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLint border, GLenum format,
                    GLenum type, const GLvoid* pixels) {
        TexImage1D_State(target, level, internalFormat, width, border, format, type, pixels);
    }

    void TexBuffer(GLenum target, GLenum internalformat, GLuint buffer) {
        TexBuffer_State(target, internalformat, buffer);
    }

    // The buffer texture bound to `target` on the active unit - what the non-DSA range form
    // operates on. Kept separate from TexBuffer_State because that one resolves the target
    // itself and attaches the whole buffer.
    static const SharedPtr<MG_State::GLState::ITextureObject>& GetBoundBufferTexture(GLenum target,
                                                                                     const char* caller) {
        TextureUploadTarget uploadTarget = MG_Util::ConvertGLEnumToTextureUploadTarget(target);
        if (!TextureImpl::ValidateTextureUploadTarget(uploadTarget)) return nullTextureObject;
        (void)caller;
        auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
        return activeUnit.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)).GetBoundObject();
    }

    void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
        // The TARGET-taking form owes GL_INVALID_ENUM for a target that is not GL_TEXTURE_BUFFER,
        // where the name-taking DSA forms below owe GL_INVALID_OPERATION for the corresponding
        // "that texture is not a buffer texture". Same shared body, different gate.
        if (!ValidateBufferTextureTarget(target, __func__)) return;
        AttachBufferToTexture(GetBoundBufferTexture(target, __func__), internalformat, buffer, offset,
                              static_cast<SizeT>(size < 0 ? 0 : size), __func__);
    }

    void TextureBuffer(GLuint texture, GLenum internalformat, GLuint buffer) {
        AttachBufferToTexture(GetTextureObjectByName(texture, __func__), internalformat, buffer, 0,
                              MG_State::GLState::TextureObjectBuffer::kWholeBuffer, __func__);
    }

    void TextureBufferRange(GLuint texture, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
        AttachBufferToTexture(GetTextureObjectByName(texture, __func__), internalformat, buffer, offset,
                              static_cast<SizeT>(size < 0 ? 0 : size), __func__);
    }

    GLboolean IsTexture(GLuint texture) {
        return IsTexture_State(texture);
    }

    void GetTexParameterIuiv(GLenum target, GLenum pname, GLuint* params) {
        GetTexParameterIuiv_State(target, pname, params);
    }

    void GetTexParameterIiv(GLenum target, GLenum pname, GLint* params) {
        GetTexParameterIiv_State(target, pname, params);
    }

    void GetTexParameteriv(GLenum target, GLenum pname, GLint* params) {
        GetTexParameteriv_State(target, pname, params);
    }

    void GetTexParameterfv(GLenum target, GLenum pname, GLfloat* params) {
        GetTexParameterfv_State(target, pname, params);
    }

    void GetTexLevelParameteriv(GLenum target, GLint level, GLenum pname, GLint* params) {
        GetTexLevelParameteriv_State(target, level, pname, params);
    }

    void GetTexLevelParameterfv(GLenum target, GLint level, GLenum pname, GLfloat* params) {
        GetTexLevelParameterfv_State(target, level, pname, params);
    }

    void GetCompressedTexImage(GLenum target, GLint level, void* img) {
        GetCompressedTexImage_State(target, level, img);
    }

    void GenTextures(GLsizei n, GLuint* textures) {
        GenTextures_State(n, textures);
    }

    void DeleteTextures(GLsizei n, const GLuint* textures) {
        DeleteTextures_State(n, textures);
    }

    void CopyTexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y,
                           GLsizei width, GLsizei height) {
        CopyTexSubImage3D_State(target, level, xoffset, yoffset, zoffset, x, y, width, height);
    }

    void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
                           GLsizei height) {
        CopyTexSubImage2D_Backend(target, level, xoffset, yoffset, x, y, width, height);
    }

    void CopyTextureSubImage2D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
                               GLsizei width, GLsizei height) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;
        // GL 4.6 sec. 8.8: the 2D form only accepts these effective targets; cube maps must
        // go through CopyTextureSubImage3D with the face as a layer.
        const auto target = textureObject->GetTarget();
        if (target != TextureTarget::Texture2D && target != TextureTarget::Texture1DArray &&
            target != TextureTarget::TextureRectangle) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "CopyTextureSubImage2D requires a 2D, 1D-array, or "
                                             "rectangle texture."));
            return;
        }
        if (!ValidateCopyTextureSubImage(textureObject, level, xoffset, yoffset, 0, width, height, 1, __func__)) {
            return;
        }
        WithTemporarilyBoundNamedTexture(textureObject, [&](GLenum glTarget) {
            CopyTexSubImage2D_Backend(glTarget, level, xoffset, yoffset, x, y, width, height);
        });
    }

    void CopyTextureSubImage1D(GLuint texture, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;
        if (textureObject->GetTarget() != TextureTarget::Texture1D) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "CopyTextureSubImage1D requires a 1D texture."));
            return;
        }
        CopyTextureSubImage1DResolved(textureObject, level, xoffset, x, y, width, __func__);
    }

    void CopyTextureSubImage3D(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x,
                               GLint y, GLsizei width, GLsizei height) {
        auto textureObject = GetTextureObjectByName(texture, __func__);
        if (!textureObject) return;
        // GL 4.6 core 8.6: the 3D form takes the layered targets, a cube map included - the face
        // is selected by zoffset.
        const auto target = textureObject->GetTarget();
        if (target != TextureTarget::Texture3D && target != TextureTarget::Texture2DArray &&
            target != TextureTarget::TextureCubeMap && target != TextureTarget::TextureCubeMapArray) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "CopyTextureSubImage3D requires a 3D, 2D-array, cube map, or "
                                             "cube map array texture."));
            return;
        }
        // A cube map addresses its faces as separate upload targets, so zoffset selects the target
        // rather than a slice within one; every other layered target keeps zoffset as the slice.
        CopyTextureSubImage3DResolved(textureObject, level, xoffset, yoffset, zoffset, x, y, width, height,
                                      /*allowCubeFaceFromZOffset=*/true, __func__);
    }

    void CopyTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width) {
        CopyTexSubImage1D_State(target, level, xoffset, x, y, width);
    }

    void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
                        GLsizei height, GLint border) {
        if (!CopyTexImage2D_State(target, level, internalformat, x, y, width, height, border)) return;
        CopyTexImage2D_Backend(target, level, internalformat, x, y, width, height, border);
    }

    void CopyTexImage1D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width,
                        GLint border) {
        CopyTexImage1D_State(target, level, internalformat, x, y, width, border);
    }

    void CopyImageSubData(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
                          GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
                          GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
        // A missing name is INVALID_VALUE here, where GetTextureObjectByName's own diagnostic is
        // INVALID_OPERATION - so resolve through the plain lookups, which answer a null
        // SharedPtr, and let the validator record the error this entry point owes.
        //
        // The TARGET picks the namespace: GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER, and a
        // renderbuffer name has nothing to do with a texture name. Resolving both through
        // GetTextureObject made every renderbuffer endpoint INVALID_VALUE - or, when the number
        // happened to collide with a live texture, INVALID_ENUM from the target check.
        const auto resolveEndpoint = [](GLuint name, GLenum target) {
            MG_Backend::CopyImageEndpoint endpoint{};
            if (target == GL_RENDERBUFFER) {
                endpoint.Renderbuffer = MG_State::pGLContext->GetRenderbufferObject(name);
            } else {
                endpoint.Texture = MG_State::pGLContext->GetTextureObject(name);
            }
            return endpoint;
        };
        const MG_Backend::CopyImageEndpoint src = resolveEndpoint(srcName, srcTarget);
        const MG_Backend::CopyImageEndpoint dst = resolveEndpoint(dstName, dstTarget);
        if (!ValidateCopyImageSubData_State(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget,
                                            dstLevel, dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth)) {
            return;
        }
        CopyImageSubData_Backend(src, srcTarget, srcLevel, srcX, srcY, srcZ, dst, dstTarget, dstLevel,
                                 dstX, dstY, dstZ, srcWidth, srcHeight, srcDepth);
    }

    void CompressedTexSubImage3D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width,
                                 GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void* data) {
        CompressedTexSubImage3D_State(target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize,
                                      data);
    }

    void CompressedTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
                                 GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
        CompressedTexSubImage2D_State(target, level, xoffset, yoffset, width, height, format, imageSize, data);
    }

    void CompressedTexSubImage1D(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
                                 GLsizei imageSize, const void* data) {
        CompressedTexSubImage1D_State(target, level, xoffset, width, format, imageSize, data);
    }

    void CompressedTexImage3D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
                              GLsizei depth, GLint border, GLsizei imageSize, const void* data) {
        CompressedTexImage3D_State(target, level, internalformat, width, height, depth, border, imageSize, data);
    }

    void CompressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
                              GLint border, GLsizei imageSize, const void* data) {
        CompressedTexImage2D_State(target, level, internalformat, width, height, border, imageSize, data);
    }

    void CompressedTexImage1D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
                              GLsizei imageSize, const void* data) {
        CompressedTexImage1D_State(target, level, internalformat, width, border, imageSize, data);
    }

    void BindTexture(GLenum target, GLuint texture) {
        BindTexture_State(target, texture);
    }

    void ActiveTexture(GLenum texture) {
        ActiveTexture_State(texture);
    }

} // namespace MobileGL::MG_Impl::GLImpl
