// MobileGL - MobileGL/MG_Impl/GLImpl/Buffer/GL_Buffer.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_Buffer.h"
#include "Validators.h"
#include "../Texture/GL_Texture.h"
#include "../Getter/GL_Getter.h"
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <Config.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/BufferEnumConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
#include <MG_Util/Texture/PixelStoreProcessor.h>

namespace MobileGL::MG_Impl::GLImpl {
    namespace {
        enum class BufferOp {
            GetBufferParameteriv,
            GetBufferParameteri64v,
            GetBufferPointerv,
            BufferStorage,
            CreateBuffers,
            NamedBufferStorage,
            NamedBufferData,
            NamedBufferSubData,
            CopyNamedBufferSubData,
            ClearBufferData,
            ClearBufferSubData,
            ClearNamedBufferData,
            ClearNamedBufferSubData,
            MapBufferRange,
            MapBuffer,
            MapNamedBuffer,
            MapNamedBufferRange,
            UnmapNamedBuffer,
            FlushMappedNamedBufferRange,
            GetNamedBufferParameteriv,
            GetNamedBufferParameteri64v,
            GetNamedBufferPointerv,
            GetNamedBufferSubData,
        };

        const char* GetBufferOpName(BufferOp op) {
            switch (op) {
            case BufferOp::GetBufferParameteriv:
                return "GetBufferParameteriv";
            case BufferOp::GetBufferParameteri64v:
                return "GetBufferParameteri64v";
            case BufferOp::GetBufferPointerv:
                return "GetBufferPointerv";
            case BufferOp::BufferStorage:
                return "BufferStorage";
            case BufferOp::CreateBuffers:
                return "CreateBuffers";
            case BufferOp::NamedBufferStorage:
                return "NamedBufferStorage";
            case BufferOp::NamedBufferData:
                return "NamedBufferData";
            case BufferOp::NamedBufferSubData:
                return "NamedBufferSubData";
            case BufferOp::CopyNamedBufferSubData:
                return "CopyNamedBufferSubData";
            case BufferOp::ClearBufferData:
                return "ClearBufferData";
            case BufferOp::ClearBufferSubData:
                return "ClearBufferSubData";
            case BufferOp::ClearNamedBufferData:
                return "ClearNamedBufferData";
            case BufferOp::ClearNamedBufferSubData:
                return "ClearNamedBufferSubData";
            case BufferOp::MapBufferRange:
                return "MapBufferRange";
            case BufferOp::MapBuffer:
                return "MapBuffer";
            case BufferOp::MapNamedBuffer:
                return "MapNamedBuffer";
            case BufferOp::MapNamedBufferRange:
                return "MapNamedBufferRange";
            case BufferOp::UnmapNamedBuffer:
                return "UnmapNamedBuffer";
            case BufferOp::FlushMappedNamedBufferRange:
                return "FlushMappedNamedBufferRange";
            case BufferOp::GetNamedBufferSubData:
                return "GetNamedBufferSubData";
            case BufferOp::GetNamedBufferParameteriv:
                return "GetNamedBufferParameteriv";
            case BufferOp::GetNamedBufferParameteri64v:
                return "GetNamedBufferParameteri64v";
            case BufferOp::GetNamedBufferPointerv:
                return "GetNamedBufferPointerv";
            default:
                return "Buffer";
            }
        }

        SharedPtr<MG_State::GLState::BufferObject> GetNamedBufferObject(GLuint buffer, BufferOp op);

        // The size of one cleared element, which is what offset and size must be multiples of
        // (GL 4.6 core 6.3). `internalformat` is restricted to the buffer-texture format table, and
        // `format`/`type` describe the client-side pattern, so both are validated here and the
        // caller only has to know how wide an element is.
        SizeT GetClearPatternSize(GLenum internalformat, GLenum format, GLenum type, BufferOp op) {
            if (!IsBufferTextureInternalFormat(internalformat)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", GetBufferOpName(op),
                        std::format("internalformat 0x{:X} is not one of the sized formats a buffer clear accepts.",
                                    internalformat)));
                return 0;
            }

            // Unlike internalformat, a bad format or type here is INVALID_VALUE rather than
            // INVALID_ENUM (GL 4.6 core 6.3) - the odd one out among the enum arguments.
            const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
            if (inputFormat == TextureInputFormat::Unknown) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 std::format("format 0x{:X} is not a pixel format.", format)));
                return 0;
            }

            const TexturePixelDataType pixelType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);
            if (pixelType == TexturePixelDataType::Unknown) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 std::format("type 0x{:X} is not a pixel type.", type)));
                return 0;
            }

            const TextureInternalFormat internal =
                MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
            const SizeT elementSize = MG_Util::GetSizedInternalFormatSizeInBytes(internal);
            if (elementSize == 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 std::format("internalformat 0x{:X} has no known element size.",
                                                             internalformat)));
                return 0;
            }

            return elementSize;
        }

        Bool ValidateBufferClearRange(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, GLintptr offset,
                                      GLsizeiptr size, SizeT patternSize, BufferOp op) {
            if (offset < 0 || size < 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "Offset and size must be non-negative."));
                return false;
            }

            if (patternSize == 0 || (static_cast<SizeT>(offset) % patternSize) != 0 ||
                (static_cast<SizeT>(size) % patternSize) != 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "Offset and size must be aligned to the clear element size."));
                return false;
            }

            if (static_cast<SizeT>(offset) + static_cast<SizeT>(size) > bufferObject->GetSize()) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "Offset and size exceed buffer size."));
                return false;
            }

            if (bufferObject->IsMapped() && !(bufferObject->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "Cannot clear a non-persistently mapped buffer object."));
                return false;
            }

            return true;
        }

        Bool BuildClearPattern(GLenum internalformat, GLenum format, GLenum type, const void* data,
                               SizeT patternSize, BufferOp op, Vector<Uint8>& pattern) {
            const TextureInternalFormat internal = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
            const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format);
            const TexturePixelDataType inputType = MG_Util::ConvertGLEnumToTexturePixelDataType(type);

            Vector<Uint8> zeroInput;
            const void* inputPixel = data;
            if (inputPixel == nullptr) {
                const SizeT inputSize = MG_Util::GetInputBytesPerPixel(inputFormat, inputType);
                if (inputSize == 0) {
                    MG_State::pGLContext->RecordError(
                        ErrorCode::InvalidValue,
                        MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                     "format and type do not describe a source pixel."));
                    return false;
                }
                zeroInput.resize(inputSize);
                inputPixel = zeroInput.data();
            }

            if (!MG_Util::PixelStoreProcessor::ConvertOnePixelToInternal(
                    internal, inputFormat, inputType, inputPixel, pattern)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", GetBufferOpName(op),
                        std::format("Cannot convert one ({}, {}) pixel into internalformat 0x{:X}.",
                                    MG_Util::ConvertGLEnumToString(format), MG_Util::ConvertGLEnumToString(type),
                                    internalformat)));
                return false;
            }

            if (data == nullptr) {
                // GL defines a null clear value as all zero bits in the destination store, while
                // retaining the format/type validation above.
                pattern.assign(patternSize, 0);
            }
            return true;
        }

        void ClearBufferRange_State(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
                                    GLenum internalformat, GLintptr offset, GLsizeiptr size,
                                    GLenum format, GLenum type, const void* data, BufferOp op) {
            const SizeT patternSize = GetClearPatternSize(internalformat, format, type, op);
            if (patternSize == 0) return;
            if (!ValidateBufferClearRange(bufferObject, offset, size, patternSize, op)) return;
            if (size == 0) return;

            Vector<Uint8> pattern;
            if (!BuildClearPattern(internalformat, format, type, data, patternSize, op, pattern)) return;
            bufferObject->FillSubData({pattern.data(), pattern.size()}, static_cast<SizeT>(offset),
                                      static_cast<SizeT>(size));
        }

        auto& GetBufferBindingSlot(BufferTarget target) {
            if (target == BufferTarget::Index) {
                return MG_State::pGLContext->GetBoundVertexArray()->GetIndexBufferBindingSlot();
            }
            return MG_State::pGLContext->GetBufferBindingSlot(target);
        }

        SharedPtr<MG_State::GLState::BufferObject> GetBoundBufferObject(GLenum target, BufferOp op) {
            BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
            if (!BufferImpl::ValidateBufferTarget(bufferTarget)) return nullptr;

            auto& bindingSlot = GetBufferBindingSlot(bufferTarget);
            auto& bufferObject = bindingSlot.GetBoundObject();
            if (!bufferObject) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "Buffer target is bound to no buffer object."));
                return nullptr;
            }
            return bufferObject;
        }

        SharedPtr<MG_State::GLState::BufferObject> GetNamedBufferObject(GLuint buffer, BufferOp op) {
            if (!BufferImpl::ValidateBufferName(buffer, false)) return nullptr;
            if (!MG_State::pGLContext->ValidateBufferObject(buffer)) {
                MG_State::pGLContext->CreateBufferObject(buffer);
            }
            auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
            if (!bufferObject) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 std::format("Buffer object {} does not exist.", buffer)));
            }
            return bufferObject;
        }

        // MOBILEGL_COHERENT_AS_FLUSH: rewrite a validated persistent FLUSH_EXPLICIT mapping
        // request to coherent semantics: the map becomes coherent-persistent (eligible for
        // the zero-copy backend map, otherwise synced wholesale at draw time), so its writes
        // reach the GPU without glFlushMappedBufferRange; flush calls on rewritten maps are
        // tolerated as no-ops by the FlushMappedBufferRange entry points. Non-persistent
        // maps keep spec FLUSH_EXPLICIT behavior on purpose: the GPU cannot read them while
        // mapped, so they gain nothing from the rewrite, and honoring only the app's flushed
        // subranges avoids clobbering GPU-written bytes elsewhere in the mapped range.
        // Runs after validation so the app's original access combination is what gets
        // validated (Coherent is injected without requiring GL_MAP_COHERENT_BIT storage).
        Flags<BufferMappingAccessBit> ApplyCoherentAsFlush(Flags<BufferMappingAccessBit> accessBits) {
            if (!MG_Config::Features.CoherentAsFlush) return accessBits;
            if (!(accessBits & BufferMappingAccessBit::FlushExplicit)) return accessBits;
            if (!(accessBits & BufferMappingAccessBit::Persistent)) return accessBits;
            accessBits = Flags<BufferMappingAccessBit>(
                accessBits.GetRaw() & ~static_cast<Uint>(BufferMappingAccessBit::FlushExplicit));
            accessBits |= BufferMappingAccessBit::Coherent;
            return accessBits;
        }

        Bool ValidateStorageFlags(GLbitfield flags, BufferOp op) {
            constexpr GLbitfield validFlags = GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT |
                                              GL_MAP_COHERENT_BIT | GL_DYNAMIC_STORAGE_BIT | GL_CLIENT_STORAGE_BIT;
            if ((flags & ~validFlags) != 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 std::format("Invalid buffer storage flags: 0x{:X}", flags)));
                return false;
            }

            if ((flags & GL_MAP_PERSISTENT_BIT) && !(flags & (GL_MAP_READ_BIT | GL_MAP_WRITE_BIT))) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "GL_MAP_PERSISTENT_BIT requires GL_MAP_READ_BIT or GL_MAP_WRITE_BIT."));
                return false;
            }

            if ((flags & GL_MAP_COHERENT_BIT) && !(flags & GL_MAP_PERSISTENT_BIT)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "GL_MAP_COHERENT_BIT requires GL_MAP_PERSISTENT_BIT."));
                return false;
            }
            return true;
        }

        Bool ValidateImmutableMapAccess(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
                                        Flags<BufferMappingAccessBit> accessBits, BufferOp op) {
            if (!bufferObject->IsImmutableStorage()) {
                if (accessBits & (BufferMappingAccessBit::Persistent | BufferMappingAccessBit::Coherent)) {
                    MG_State::pGLContext->RecordError(
                        ErrorCode::InvalidOperation,
                        MakeUnique<GenericErrorInfo>(
                            "MG_Impl/GLImpl", GetBufferOpName(op),
                            "Persistent or coherent mapping requires immutable buffer storage."));
                    return false;
                }
                return true;
            }

            const GLbitfield storageFlags = bufferObject->GetStorageFlags();
            if ((accessBits & BufferMappingAccessBit::Read) && !(storageFlags & GL_MAP_READ_BIT)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "GL_MAP_READ_BIT is not allowed by buffer storage flags."));
                return false;
            }
            if ((accessBits & BufferMappingAccessBit::Write) && !(storageFlags & GL_MAP_WRITE_BIT)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "GL_MAP_WRITE_BIT is not allowed by buffer storage flags."));
                return false;
            }
            if ((accessBits & BufferMappingAccessBit::Persistent) && !(storageFlags & GL_MAP_PERSISTENT_BIT)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "GL_MAP_PERSISTENT_BIT is not allowed by buffer storage flags."));
                return false;
            }
            if ((accessBits & BufferMappingAccessBit::Coherent) && !(storageFlags & GL_MAP_COHERENT_BIT)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                 "GL_MAP_COHERENT_BIT is not allowed by buffer storage flags."));
                return false;
            }
            return true;
        }

        void GetBufferParameteriv_Object(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, GLenum pname,
                                         GLint* params, BufferOp op) {
            if (!params) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                                          "Params pointer cannot be null."));
                return;
            }

            switch (pname) {
            case GL_BUFFER_SIZE:
                *params = static_cast<GLint>(bufferObject->GetSize());
                break;
            case GL_BUFFER_USAGE:
                *params = (GLint)MG_Util::ConvertBufferUsageToGLEnum(bufferObject->GetUsage());
                break;
            case GL_BUFFER_ACCESS:
                if (bufferObject->IsMapped()) {
                    auto access = bufferObject->GetMappingAccess();
                    if (access & BufferMappingAccessBit::Read && access & BufferMappingAccessBit::Write) {
                        *params = GL_READ_WRITE;
                    } else if (access & BufferMappingAccessBit::Read) {
                        *params = GL_READ_ONLY;
                    } else if (access & BufferMappingAccessBit::Write) {
                        *params = GL_WRITE_ONLY;
                    } else {
                        *params = GL_READ_WRITE;
                    }
                } else {
                    // Initial value, and what glUnmapBuffer restores (GL 4.6 core table 6.2).
                    *params = GL_READ_WRITE;
                }
                break;
            case GL_BUFFER_ACCESS_FLAGS:
                // The MapBufferRange flags verbatim; glMapBuffer's access enum has already been
                // normalised into the same bits. Zero while the buffer is not mapped.
                *params = bufferObject->IsMapped()
                    ? static_cast<GLint>(
                          MG_Util::ConvertBufferMappingAccessToGLEnum(bufferObject->GetMappingAccess()))
                    : 0;
                break;
            case GL_BUFFER_MAPPED:
                *params = bufferObject->IsMapped() ? GL_TRUE : GL_FALSE;
                break;
            case GL_BUFFER_IMMUTABLE_STORAGE:
                *params = bufferObject->IsImmutableStorage() ? GL_TRUE : GL_FALSE;
                break;
            case GL_BUFFER_STORAGE_FLAGS:
                *params = static_cast<GLint>(bufferObject->GetStorageFlags());
                break;
            case GL_BUFFER_MAP_OFFSET:
                *params = static_cast<GLint>(bufferObject->GetMappedRange().start);
                break;
            case GL_BUFFER_MAP_LENGTH:
                *params = static_cast<GLint>(bufferObject->GetMappedRange().end - bufferObject->GetMappedRange().start);
                break;
            default:
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                                         std::format("Invalid pname enum: 0x{:X}", pname)));
                break;
            }
        }

        void GetBufferParameteri64v_Object(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, GLenum pname,
                                           GLint64* params, BufferOp op) {
            if (!params) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                                          "Params pointer cannot be null."));
                return;
            }

            switch (pname) {
            case GL_BUFFER_SIZE:
                *params = static_cast<GLint64>(bufferObject->GetSize());
                break;
            case GL_BUFFER_MAP_OFFSET:
                *params = static_cast<GLint64>(bufferObject->GetMappedRange().start);
                break;
            case GL_BUFFER_MAP_LENGTH:
                *params = static_cast<GLint64>(bufferObject->GetMappedRange().end - bufferObject->GetMappedRange().start);
                break;
            default: {
                GLint value = 0;
                GetBufferParameteriv_Object(bufferObject, pname, &value, op);
                *params = static_cast<GLint64>(value);
                break;
            }
            }
        }

        void GetBufferPointerv_Object(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject, GLenum pname,
                                      void** params, BufferOp op) {
            if (!params) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                                          "Params pointer cannot be null."));
                return;
            }
            if (pname != GL_BUFFER_MAP_POINTER) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidEnum, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", GetBufferOpName(op),
                                                                         std::format("Invalid pname enum: 0x{:X}", pname)));
                return;
            }
            *params = bufferObject->GetMappedPointer();
        }
    } // namespace

    void GetBufferParameteriv_State(GLenum target, GLenum pname, GLint* params) {
        auto bufferObject = GetBoundBufferObject(target, BufferOp::GetBufferParameteriv);
        if (!bufferObject) return;
        GetBufferParameteriv_Object(bufferObject, pname, params, BufferOp::GetBufferParameteriv);
    }

    void GetBufferParameteri64v_State(GLenum target, GLenum pname, GLint64* params) {
        auto bufferObject = GetBoundBufferObject(target, BufferOp::GetBufferParameteri64v);
        if (!bufferObject) return;
        GetBufferParameteri64v_Object(bufferObject, pname, params, BufferOp::GetBufferParameteri64v);
    }

    void GetBufferPointerv_State(GLenum target, GLenum pname, void** params) {
        auto bufferObject = GetBoundBufferObject(target, BufferOp::GetBufferPointerv);
        if (!bufferObject) return;
        GetBufferPointerv_Object(bufferObject, pname, params, BufferOp::GetBufferPointerv);
    }

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

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

        for (SizeT i = 0; i < static_cast<SizeT>(n); ++i) {
            Uint bufferName = buffers[i];
            if (bufferName == 0) continue;
            // GL 3.3 core 2.9: names that do not correspond to an existing buffer are silently
            // ignored here, so probe with the non-recording query - the shared validator would
            // record INVALID_OPERATION, which is only correct on the bind path.
            if (!MG_State::pGLContext->ValidateBufferName(bufferName)) continue;
            MG_State::pGLContext->MarkBufferObjectForDeletion(bufferName);
        }
    }

    void FlushMappedBufferRange_State(GLenum target, GLintptr offset, GLsizeiptr length) {
        if (length < 0 || offset < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "FlushMappedBufferRange_State",
                                                                      "Offset and length must be non-negative."));
            return;
        }

        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferTarget(bufferTarget)) return;

        auto& bindingSlot = GetBufferBindingSlot(bufferTarget);

        auto& bufferObject = bindingSlot.GetBoundObject();
        if (!bufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "FlushMappedBufferRange_State",
                                             "Buffer target is bound to no buffer object."));
            return;
        }

        if (!bufferObject->IsMapped()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "FlushMappedBufferRange_State",
                                             "Cannot flush a buffer object that is not mapped."));
            return;
        }

        const auto mappedRange = bufferObject->GetMappedRange();
        if (static_cast<SizeT>(offset) + static_cast<SizeT>(length) > mappedRange.end - mappedRange.start) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue, MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "FlushMappedBufferRange_State",
                                                                      "Offset and length exceed mapped range."));
            return;
        }

        auto mappingAccess = bufferObject->GetMappingAccess();
        if (!(mappingAccess & BufferMappingAccessBit::FlushExplicit)) {
            // MOBILEGL_COHERENT_AS_FLUSH strips FLUSH_EXPLICIT from persistent maps at map
            // time (leaving Persistent|Coherent), so honor the app's flush on such a map as
            // a no-op: its writes reach the backend without explicit flushes. Other maps
            // keep the spec error.
            if (MG_Config::Features.CoherentAsFlush &&
                (mappingAccess & BufferMappingAccessBit::Persistent) &&
                (mappingAccess & BufferMappingAccessBit::Coherent)) {
                return;
            }
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", "FlushMappedBufferRange_State",
                    "Cannot flush a buffer object that is not mapped with GL_MAP_FLUSH_EXPLICIT_BIT."));
            return;
        }

        bufferObject->FlushMemoryRange(static_cast<SizeT>(offset), static_cast<SizeT>(length));
    }

    GLboolean UnmapBuffer_State(GLenum target) {
        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferTarget(bufferTarget)) return GL_FALSE;

        auto& bindingSlot = GetBufferBindingSlot(bufferTarget);

        auto& bufferObject = bindingSlot.GetBoundObject();
        if (!bufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "UnmapBuffer_State",
                                             "Buffer target is bound to no buffer object."));
            return GL_FALSE;
        }

        if (!bufferObject->IsMapped()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "UnmapBuffer_State",
                                             "Cannot unmap a buffer object that is not mapped."));
            return GL_FALSE;
        }

        bufferObject->ReleaseMemory();
        return GL_TRUE;
    }

    void* MapBufferRange_State(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) {
        if (length < 0 || offset < 0) {
            MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                                                           "Offset and length must be non-negative."));
            return nullptr;
        }

        if (length == 0) {
            MG_State::pGLContext->RecordError(ErrorCode::InvalidOperation,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                                                           "Length must be greater than zero."));
            return nullptr;
        }

        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferTarget(bufferTarget)) return nullptr;

        auto& bindingSlot = GetBufferBindingSlot(bufferTarget);
        auto& bufferObject = bindingSlot.GetBoundObject();
        if (!bufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                             "Buffer target is bound to no buffer object."));
            return nullptr;
        }

        if (offset + length > bufferObject->GetSize()) {
            MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                                                           "Offset and length exceed buffer size."));
            return nullptr;
        }

        auto accessBits = MG_Util::ConvertGLEnumToBufferMappingAccess(access);
        if (!BufferImpl::ValidateBufferMappingAccess(accessBits)) return nullptr;

        if (!(accessBits & (BufferMappingAccessBit::Read | BufferMappingAccessBit::Write))) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                             "At least one of GL_MAP_READ_BIT or GL_MAP_WRITE_BIT must be set."));
            return nullptr;
        }

        if (accessBits & BufferMappingAccessBit::Read) {
            const auto invalidFlags = BufferMappingAccessBit::InvalidateRange |
                                      BufferMappingAccessBit::InvalidateBuffer | BufferMappingAccessBit::Unsynchronized;

            if (accessBits & invalidFlags) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", "MapBufferRange_State",
                        "GL_MAP_READ_BIT cannot be combined with invalidation or unsynchronized flags."));
                return nullptr;
            }
        }

        if (accessBits & BufferMappingAccessBit::FlushExplicit) {
            if (!(accessBits & BufferMappingAccessBit::Write)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                                 "GL_MAP_FLUSH_EXPLICIT_BIT requires GL_MAP_WRITE_BIT."));
                return nullptr;
            }
        }

        if ((accessBits & BufferMappingAccessBit::Persistent) && !(accessBits & (BufferMappingAccessBit::Read | BufferMappingAccessBit::Write))) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                             "GL_MAP_PERSISTENT_BIT requires GL_MAP_READ_BIT or GL_MAP_WRITE_BIT."));
            return nullptr;
        }

        if ((accessBits & BufferMappingAccessBit::Coherent) && !(accessBits & BufferMappingAccessBit::Persistent)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                             "GL_MAP_COHERENT_BIT requires GL_MAP_PERSISTENT_BIT."));
            return nullptr;
        }

        if (!ValidateImmutableMapAccess(bufferObject, accessBits, BufferOp::MapBufferRange)) return nullptr;

        if (bufferObject->IsMapped()) {
            const auto invalidateFlags =
                BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer;

            if (!(accessBits & invalidateFlags)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                                 "Cannot map a buffer object that is already mapped."));
                return nullptr;
            }
        }

        void* result = bufferObject->AcquireMemoryRange(
            {static_cast<SizeT>(offset), static_cast<SizeT>(offset + length)}, ApplyCoherentAsFlush(accessBits));
        if (!result) {
            MG_State::pGLContext->RecordError(
                ErrorCode::OutOfMemory,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBufferRange_State",
                                             "Failed to map buffer due to insufficient memory."));
            return nullptr;
        }
        return result;
    }

    void* MapBuffer_State(GLenum target, GLenum access) {
        Bool readable = access == GL_READ_ONLY || access == GL_READ_WRITE;
        Bool writable = access == GL_WRITE_ONLY || access == GL_READ_WRITE;
        if (access != GL_READ_ONLY && access != GL_WRITE_ONLY && access != GL_READ_WRITE) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBuffer_State",
                                             "Access must be one of GL_READ_ONLY, GL_WRITE_ONLY, or GL_READ_WRITE."));
            return nullptr;
        }

        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferTarget(bufferTarget)) return nullptr;
        auto& bindingSlot = GetBufferBindingSlot(bufferTarget);

        auto& bufferObject = bindingSlot.GetBoundObject();
        if (!bufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBuffer_State",
                                             "Buffer target is bound to no buffer object."));
            return nullptr;
        }

        if (bufferObject->IsMapped()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBuffer_State",
                                             "Cannot map a buffer object that is already mapped."));
            return nullptr;
        }

        Flags<BufferMappingAccessBit> accessBits = BufferMappingAccessBit::Null;
        if (readable) accessBits |= BufferMappingAccessBit::Read;
        if (writable) accessBits |= BufferMappingAccessBit::Write;
        if (!ValidateImmutableMapAccess(bufferObject, accessBits, BufferOp::MapBuffer)) return nullptr;

        void* result = bufferObject->AcquireMemory(true, readable, writable);
        if (!result) {
            MG_State::pGLContext->RecordError(
                ErrorCode::OutOfMemory,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapBuffer_State",
                                             "Failed to map buffer due to insufficient memory."));
            return nullptr;
        }
        return result;
    }

    void CopyBufferSubData_State(GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset,
                                 GLsizeiptr size) {
        if (size < 0 || readOffset < 0 || writeOffset < 0) {
            MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyBufferSubData_State",
                                                                           "Offset and size must be non-negative."));
            return;
        }

        BufferTarget readBufferTarget = MG_Util::ConvertGLEnumToBufferTarget(readTarget);
        BufferTarget writeBufferTarget = MG_Util::ConvertGLEnumToBufferTarget(writeTarget);
        if (!BufferImpl::ValidateBufferTarget(readBufferTarget) || !BufferImpl::ValidateBufferTarget(writeBufferTarget))
            return;

        auto& readBindingSlot = GetBufferBindingSlot(readBufferTarget);
        auto& writeBindingSlot = GetBufferBindingSlot(writeBufferTarget);
        auto& readBufferObject = readBindingSlot.GetBoundObject();
        auto& writeBufferObject = writeBindingSlot.GetBoundObject();

        if (!readBufferObject || !writeBufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyBufferSubData_State",
                                             "One of the buffer targets is bound to no buffer object."));
            return;
        }

        if (readOffset + size > readBufferObject->GetSize() || writeOffset + size > writeBufferObject->GetSize()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyBufferSubData_State",
                                             "Offset and size must be within the bounds of the buffer objects."));
            return;
        }

        if (readBufferObject == writeBufferObject) {
            if ((readOffset <= writeOffset && readOffset + size > writeOffset) ||
                (writeOffset <= readOffset && writeOffset + size > readOffset)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyBufferSubData_State",
                                                 "Source and destination buffers overlap in the specified ranges."));
                return;
            }
        }

        auto isIllegallyMapped = [](const SharedPtr<MG_State::GLState::BufferObject>& buffer) {
            return buffer->IsMapped() && !(buffer->GetMappingAccess() & BufferMappingAccessBit::Persistent);
        };
        if (isIllegallyMapped(readBufferObject) || isIllegallyMapped(writeBufferObject)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyBufferSubData_State",
                                             "Cannot copy data from/to a mapped buffer object unless it was mapped "
                                             "with GL_MAP_PERSISTENT_BIT."));
            return;
        }

        writeBufferObject->CopyDataFrom(readBufferObject, readOffset, writeOffset, size);
    }

    void BufferSubData_State(GLenum target, GLintptr offset, GLsizeiptr size, const void* data) {
        MGLOG_D("%s: target = %s, offset = %d, size = %d, data = %p", __func__,
                MG_Util::ConvertGLEnumToString(target).c_str(), offset, size, data);
        if (!data) {
            MG_State::pGLContext->RecordError(
                ErrorCode::NoError, // somehow OpenGL does not generate an error for this
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferSubData_State", "Data pointer cannot be null."));
            return;
        }

        if (size < 0 || offset < 0) {
            MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferSubData_State",
                                                                           "Offset and size must be non-negative."));
            return;
        }

        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferTarget(bufferTarget)) return;
        auto& bindingSlot = GetBufferBindingSlot(bufferTarget);

        auto& bufferObject = bindingSlot.GetBoundObject();
        if (!bufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferSubData_State",
                                             "Buffer target is bound to no buffer object."));
            return;
        }

        if (bufferObject->IsImmutableStorage() && !(bufferObject->GetStorageFlags() & GL_DYNAMIC_STORAGE_BIT)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferSubData_State",
                                             "Immutable buffer storage was not created with GL_DYNAMIC_STORAGE_BIT."));
            return;
        }

        SizeT bufferSize = bufferObject->GetSize();
        if (static_cast<SizeT>(offset) + static_cast<SizeT>(size) > bufferSize) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferSubData_State",
                                             "Offset and size exceed buffer size."));
            return;
        }

        Range1D mappedRange = bufferObject->GetMappedRange();
        auto mappingAccess = bufferObject->GetMappingAccess();
        // GL 4.6 6.5: the error is on OVERLAP with the mapped range, i.e. a half-open
        // intersection test. There used to be a second test below this one asking only
        // `offset + size >= mappedRange.start`, which rejects every write that starts
        // before a mapped tail as well - it made a legal disjoint glBufferSubData fail.
        if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent) &&
            (offset < mappedRange.end) && (offset + size > mappedRange.start)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", "BufferSubData_State",
                    "Cannot modify a non-persistently mapped buffer object."));
            return;
        }

        bufferObject->UploadSubData({(void*)data, (SizeT)size}, offset);
    }

    void GetBufferSubData_State(GLenum target, GLintptr offset, GLsizeiptr size, void* data) {
        MGLOG_D("%s: target = %s, offset = %d, size = %d, data = %p", __func__,
                MG_Util::ConvertGLEnumToString(target).c_str(), offset, size, data);
        if (!data) {
            // Match BufferSubData_State: a null pointer is a caller bug, not a GL-specified error.
            return;
        }

        if (size < 0 || offset < 0) {
            MG_State::pGLContext->RecordError(ErrorCode::InvalidValue,
                                              MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetBufferSubData_State",
                                                                           "Offset and size must be non-negative."));
            return;
        }

        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferTarget(bufferTarget)) return;
        auto& bindingSlot = GetBufferBindingSlot(bufferTarget);

        auto& bufferObject = bindingSlot.GetBoundObject();
        if (!bufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetBufferSubData_State",
                                             "Buffer target is bound to no buffer object."));
            return;
        }

        SizeT bufferSize = bufferObject->GetSize();
        if (static_cast<SizeT>(offset) + static_cast<SizeT>(size) > bufferSize) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetBufferSubData_State",
                                             "Offset and size exceed buffer size."));
            return;
        }

        if (bufferObject->IsMapped() &&
            !(bufferObject->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetBufferSubData_State",
                                             "Cannot read from a buffer object mapped without GL_MAP_PERSISTENT_BIT."));
            return;
        }

        bufferObject->SyncGpuWrites();
        bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
    }

    void GetNamedBufferSubData_State(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) {
        if (!data) {
            // Match GetBufferSubData_State: a null pointer is a caller bug, not a GL-specified error.
            return;
        }

        if (size < 0 || offset < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
                                             "Offset and size must be non-negative."));
            return;
        }

        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::GetNamedBufferSubData);
        if (!bufferObject) return;

        if (static_cast<SizeT>(offset) + static_cast<SizeT>(size) > bufferObject->GetSize()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
                                             "Offset and size exceed buffer size."));
            return;
        }

        if (bufferObject->IsMapped() &&
            !(bufferObject->GetMappingAccess() & BufferMappingAccessBit::Persistent)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GetNamedBufferSubData_State",
                                             "Cannot read from a buffer object mapped without GL_MAP_PERSISTENT_BIT."));
            return;
        }

        bufferObject->SyncGpuWrites();
        bufferObject->DownloadSubData(data, static_cast<SizeT>(offset), static_cast<SizeT>(size));
    }

    void BufferData_State(GLenum target, GLsizeiptr size, const void* data, GLenum usage) {
        MGLOG_D("%s: %s, size = %d, data = %p, usage = %s", __func__, MG_Util::ConvertGLEnumToString(target).c_str(),
                size, data, MG_Util::ConvertGLEnumToString(usage).c_str());
        if (size < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferData_State", "Size must be non-negative."));
            return;
        }

        BufferUsage bufferUsage = MG_Util::ConvertGLEnumToBufferUsage(usage);
        if (!BufferImpl::ValidateBufferUsage(bufferUsage)) return;

        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferTarget(bufferTarget)) return;
        auto& bindingSlot = GetBufferBindingSlot(bufferTarget);

        auto& bufferObject = bindingSlot.GetBoundObject();
        if (!bufferObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferData_State",
                                             "Buffer target is bound to no buffer object."));
            return;
        }

        if (bufferObject->IsImmutableStorage()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferData_State",
                                             "Cannot call glBufferData on immutable buffer storage."));
            return;
        }

        bufferObject->SetUsage(bufferUsage);
        bufferObject->Respecify(size, data);
    }

    void BufferStorage_State(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) {
        // Error precedence: "no buffer is bound to target" outranks a bad size or bad
        // flags, so the binding has to be resolved before either is validated.
        auto bufferObject = GetBoundBufferObject(target, BufferOp::BufferStorage);
        if (!bufferObject) return;

        if (size <= 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferStorage_State", "Size must be positive."));
            return;
        }
        if (!ValidateStorageFlags(flags, BufferOp::BufferStorage)) return;

        if (bufferObject->IsImmutableStorage()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "BufferStorage_State",
                                             "Buffer already has immutable storage."));
            return;
        }
        bufferObject->AllocateImmutableStorage(static_cast<SizeT>(size), data, flags);
    }

    void CreateBuffers_State(GLsizei n, GLuint* buffers) {
        if (n < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CreateBuffers_State", "Count must be non-negative."));
            return;
        }
        if (n > 0 && !buffers) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CreateBuffers_State",
                                             "Buffer output pointer cannot be null."));
            return;
        }

        Vector<Uint> bufferNames;
        MG_State::pGLContext->GenBufferNames(static_cast<SizeT>(n), bufferNames);
        for (GLsizei i = 0; i < n; ++i) {
            buffers[i] = bufferNames[i];
            MG_State::pGLContext->CreateBufferObject(bufferNames[i]);
        }
    }

    void NamedBufferStorage_State(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) {
        // Same precedence as BufferStorage_State: the buffer-name error comes first.
        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferStorage);
        if (!bufferObject) return;

        if (size <= 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedBufferStorage_State", "Size must be positive."));
            return;
        }
        if (!ValidateStorageFlags(flags, BufferOp::NamedBufferStorage)) return;

        if (bufferObject->IsImmutableStorage()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedBufferStorage_State",
                                             "Buffer already has immutable storage."));
            return;
        }
        bufferObject->AllocateImmutableStorage(static_cast<SizeT>(size), data, flags);
    }

    void NamedBufferData_State(GLuint buffer, GLsizeiptr size, const void* data, GLenum usage) {
        if (size < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedBufferData_State", "Size must be non-negative."));
            return;
        }

        BufferUsage bufferUsage = MG_Util::ConvertGLEnumToBufferUsage(usage);
        if (!BufferImpl::ValidateBufferUsage(bufferUsage)) return;

        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferData);
        if (!bufferObject) return;

        if (bufferObject->IsImmutableStorage()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedBufferData_State",
                                             "Cannot call glNamedBufferData on immutable buffer storage."));
            return;
        }

        bufferObject->SetUsage(bufferUsage);
        bufferObject->Respecify(size, data);
    }

    void NamedBufferSubData_State(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data) {
        if (!data) {
            MG_State::pGLContext->RecordError(
                ErrorCode::NoError,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedBufferSubData_State",
                                             "Data pointer cannot be null."));
            return;
        }
        if (size < 0 || offset < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedBufferSubData_State",
                                             "Offset and size must be non-negative."));
            return;
        }

        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::NamedBufferSubData);
        if (!bufferObject) return;

        if (bufferObject->IsImmutableStorage() && !(bufferObject->GetStorageFlags() & GL_DYNAMIC_STORAGE_BIT)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedBufferSubData_State",
                                             "Immutable buffer storage was not created with GL_DYNAMIC_STORAGE_BIT."));
            return;
        }
        if (static_cast<SizeT>(offset) + static_cast<SizeT>(size) > bufferObject->GetSize()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedBufferSubData_State",
                                             "Offset and size exceed buffer size."));
            return;
        }
        const auto mappingAccess = bufferObject->GetMappingAccess();
        const auto mappedRange = bufferObject->GetMappedRange();
        if (bufferObject->IsMapped() && !(mappingAccess & BufferMappingAccessBit::Persistent) &&
            (offset < mappedRange.end) && (offset + size > mappedRange.start)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "NamedBufferSubData_State",
                                             "Cannot modify a non-persistently mapped buffer object."));
            return;
        }

        bufferObject->UploadSubData({(void*)data, (SizeT)size}, offset);
    }

    void CopyNamedBufferSubData_State(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
                                      GLsizeiptr size) {
        if (size < 0 || readOffset < 0 || writeOffset < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyNamedBufferSubData_State",
                                             "Offset and size must be non-negative."));
            return;
        }

        auto readBufferObject = GetNamedBufferObject(readBuffer, BufferOp::CopyNamedBufferSubData);
        auto writeBufferObject = GetNamedBufferObject(writeBuffer, BufferOp::CopyNamedBufferSubData);
        if (!readBufferObject || !writeBufferObject) return;

        if (static_cast<SizeT>(readOffset) + static_cast<SizeT>(size) > readBufferObject->GetSize() ||
            static_cast<SizeT>(writeOffset) + static_cast<SizeT>(size) > writeBufferObject->GetSize()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyNamedBufferSubData_State",
                                             "Offset and size must be within the bounds of the buffer objects."));
            return;
        }

        if (readBufferObject == writeBufferObject) {
            if ((readOffset <= writeOffset && readOffset + size > writeOffset) ||
                (writeOffset <= readOffset && writeOffset + size > readOffset)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyNamedBufferSubData_State",
                                                 "Source and destination ranges overlap."));
                return;
            }
        }

        auto isIllegallyMapped = [](const SharedPtr<MG_State::GLState::BufferObject>& buffer) {
            return buffer->IsMapped() && !(buffer->GetMappingAccess() & BufferMappingAccessBit::Persistent);
        };
        if (isIllegallyMapped(readBufferObject) || isIllegallyMapped(writeBufferObject)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "CopyNamedBufferSubData_State",
                                             "Cannot copy data from/to a non-persistently mapped buffer object."));
            return;
        }

        writeBufferObject->CopyDataFrom(readBufferObject, static_cast<SizeT>(readOffset),
                                        static_cast<SizeT>(writeOffset), static_cast<SizeT>(size));
    }

    void ClearBufferData_State(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
        auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferData);
        if (!bufferObject) return;
        ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
                               type, data, BufferOp::ClearBufferData);
    }

    void ClearBufferSubData_State(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size,
                                  GLenum format, GLenum type, const void* data) {
        auto bufferObject = GetBoundBufferObject(target, BufferOp::ClearBufferSubData);
        if (!bufferObject) return;
        ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
                               BufferOp::ClearBufferSubData);
    }

    void ClearNamedBufferData_State(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferData);
        if (!bufferObject) return;
        ClearBufferRange_State(bufferObject, internalformat, 0, static_cast<GLsizeiptr>(bufferObject->GetSize()), format,
                               type, data, BufferOp::ClearNamedBufferData);
    }

    void ClearNamedBufferSubData_State(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size,
                                       GLenum format, GLenum type, const void* data) {
        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::ClearNamedBufferSubData);
        if (!bufferObject) return;
        ClearBufferRange_State(bufferObject, internalformat, offset, size, format, type, data,
                               BufferOp::ClearNamedBufferSubData);
    }

    void* MapNamedBuffer_State(GLuint buffer, GLenum access) {
        Bool readable = access == GL_READ_ONLY || access == GL_READ_WRITE;
        Bool writable = access == GL_WRITE_ONLY || access == GL_READ_WRITE;
        if (access != GL_READ_ONLY && access != GL_WRITE_ONLY && access != GL_READ_WRITE) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBuffer_State",
                                             "Access must be one of GL_READ_ONLY, GL_WRITE_ONLY, or GL_READ_WRITE."));
            return nullptr;
        }

        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::MapNamedBuffer);
        if (!bufferObject) return nullptr;
        if (bufferObject->IsMapped()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBuffer_State",
                                             "Cannot map a buffer object that is already mapped."));
            return nullptr;
        }

        Flags<BufferMappingAccessBit> accessBits = BufferMappingAccessBit::Null;
        if (readable) accessBits |= BufferMappingAccessBit::Read;
        if (writable) accessBits |= BufferMappingAccessBit::Write;
        if (!ValidateImmutableMapAccess(bufferObject, accessBits, BufferOp::MapNamedBuffer)) return nullptr;

        return bufferObject->AcquireMemory(true, readable, writable);
    }

    void* MapNamedBufferRange_State(GLuint buffer, GLintptr offset, GLsizeiptr length, GLbitfield access) {
        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::MapNamedBufferRange);
        if (!bufferObject) return nullptr;

        if (length < 0 || offset < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBufferRange_State",
                                             "Offset and length must be non-negative."));
            return nullptr;
        }
        if (length == 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBufferRange_State",
                                             "Length must be greater than zero."));
            return nullptr;
        }
        if (static_cast<SizeT>(offset) + static_cast<SizeT>(length) > bufferObject->GetSize()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBufferRange_State",
                                             "Offset and length exceed buffer size."));
            return nullptr;
        }

        auto accessBits = MG_Util::ConvertGLEnumToBufferMappingAccess(access);
        if (!BufferImpl::ValidateBufferMappingAccess(accessBits)) return nullptr;
        if (!(accessBits & (BufferMappingAccessBit::Read | BufferMappingAccessBit::Write))) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBufferRange_State",
                                             "At least one of GL_MAP_READ_BIT or GL_MAP_WRITE_BIT must be set."));
            return nullptr;
        }
        if (accessBits & BufferMappingAccessBit::Read) {
            const auto invalidFlags = BufferMappingAccessBit::InvalidateRange |
                                      BufferMappingAccessBit::InvalidateBuffer | BufferMappingAccessBit::Unsynchronized;
            if (accessBits & invalidFlags) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBufferRange_State",
                                                 "GL_MAP_READ_BIT cannot be combined with invalidation or unsynchronized flags."));
                return nullptr;
            }
        }
        if ((accessBits & BufferMappingAccessBit::FlushExplicit) && !(accessBits & BufferMappingAccessBit::Write)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBufferRange_State",
                                             "GL_MAP_FLUSH_EXPLICIT_BIT requires GL_MAP_WRITE_BIT."));
            return nullptr;
        }
        if ((accessBits & BufferMappingAccessBit::Persistent) && !(accessBits & (BufferMappingAccessBit::Read | BufferMappingAccessBit::Write))) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBufferRange_State",
                                             "GL_MAP_PERSISTENT_BIT requires GL_MAP_READ_BIT or GL_MAP_WRITE_BIT."));
            return nullptr;
        }
        if ((accessBits & BufferMappingAccessBit::Coherent) && !(accessBits & BufferMappingAccessBit::Persistent)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBufferRange_State",
                                             "GL_MAP_COHERENT_BIT requires GL_MAP_PERSISTENT_BIT."));
            return nullptr;
        }
        if (!ValidateImmutableMapAccess(bufferObject, accessBits, BufferOp::MapNamedBufferRange)) return nullptr;

        if (bufferObject->IsMapped()) {
            const auto invalidateFlags =
                BufferMappingAccessBit::InvalidateRange | BufferMappingAccessBit::InvalidateBuffer;
            if (!(accessBits & invalidateFlags)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "MapNamedBufferRange_State",
                                                 "Cannot map a buffer object that is already mapped."));
                return nullptr;
            }
        }

        return bufferObject->AcquireMemoryRange({static_cast<SizeT>(offset), static_cast<SizeT>(offset + length)},
                                                ApplyCoherentAsFlush(accessBits));
    }

    GLboolean UnmapNamedBuffer_State(GLuint buffer) {
        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::UnmapNamedBuffer);
        if (!bufferObject) return GL_FALSE;
        if (!bufferObject->IsMapped()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "UnmapNamedBuffer_State",
                                             "Cannot unmap a buffer object that is not mapped."));
            return GL_FALSE;
        }
        bufferObject->ReleaseMemory();
        return GL_TRUE;
    }

    void FlushMappedNamedBufferRange_State(GLuint buffer, GLintptr offset, GLsizeiptr length) {
        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::FlushMappedNamedBufferRange);
        if (!bufferObject) return;
        if (length < 0 || offset < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "FlushMappedNamedBufferRange_State",
                                             "Offset and length must be non-negative."));
            return;
        }
        if (!bufferObject->IsMapped()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "FlushMappedNamedBufferRange_State",
                                             "Cannot flush a buffer object that is not mapped."));
            return;
        }
        const auto mappedRange = bufferObject->GetMappedRange();
        if (static_cast<SizeT>(offset) + static_cast<SizeT>(length) > mappedRange.end - mappedRange.start) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "FlushMappedNamedBufferRange_State",
                                             "Offset and length exceed mapped range."));
            return;
        }
        const auto namedMappingAccess = bufferObject->GetMappingAccess();
        if (!(namedMappingAccess & BufferMappingAccessBit::FlushExplicit)) {
            // See FlushMappedBufferRange_State: rewritten coherent-as-flush persistent maps
            // tolerate app flushes as no-ops.
            if (MG_Config::Features.CoherentAsFlush &&
                (namedMappingAccess & BufferMappingAccessBit::Persistent) &&
                (namedMappingAccess & BufferMappingAccessBit::Coherent)) {
                return;
            }
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "FlushMappedNamedBufferRange_State",
                                             "Cannot flush a buffer object that is not mapped with GL_MAP_FLUSH_EXPLICIT_BIT."));
            return;
        }
        bufferObject->FlushMemoryRange(static_cast<SizeT>(offset), static_cast<SizeT>(length));
    }

    void GetNamedBufferParameteriv_State(GLuint buffer, GLenum pname, GLint* params) {
        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::GetNamedBufferParameteriv);
        if (!bufferObject) return;
        GetBufferParameteriv_Object(bufferObject, pname, params, BufferOp::GetNamedBufferParameteriv);
    }

    void GetNamedBufferParameteri64v_State(GLuint buffer, GLenum pname, GLint64* params) {
        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::GetNamedBufferParameteri64v);
        if (!bufferObject) return;
        GetBufferParameteri64v_Object(bufferObject, pname, params, BufferOp::GetNamedBufferParameteri64v);
    }

    void GetNamedBufferPointerv_State(GLuint buffer, GLenum pname, void** params) {
        auto bufferObject = GetNamedBufferObject(buffer, BufferOp::GetNamedBufferPointerv);
        if (!bufferObject) return;
        GetBufferPointerv_Object(bufferObject, pname, params, BufferOp::GetNamedBufferPointerv);
    }

    void BindBuffer_State(GLenum target, GLuint buffer) {
        if (!BufferImpl::ValidateBufferName(buffer, true)) return;
        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferTarget(bufferTarget)) return;

        SharedPtr<MG_State::GLState::BufferObject> bufferObject;
        if (buffer != 0) {
            Bool doesBufferObjectCreated = MG_State::pGLContext->ValidateBufferObject(buffer);
            if (!doesBufferObjectCreated) {
                MG_State::pGLContext->CreateBufferObject(buffer);
            }
            bufferObject = MG_State::pGLContext->GetBufferObject(buffer);
        }

        auto& bindingSlot = GetBufferBindingSlot(bufferTarget);
        bindingSlot.Bind(bufferObject);
        MGLOG_D("%s: bind buffer object %d -> %s", __func__, bufferObject ? bufferObject->GetExternalIndex() : 0,
                MG_Util::ConvertGLEnumToString(target).c_str());
    }

    void GenBuffers_State(GLsizei n, GLuint* buffers) {
        if (n < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", "GenBuffers_State", "n must be non-negative"));
            return;
        }
        Vector<GLuint> bufferNames;
        MG_State::pGLContext->GenBufferNames(n, bufferNames);
        Memcpy(buffers, bufferNames.data(), n * sizeof(GLuint));
    }

    GLboolean IsBuffer_State(GLuint buffer) {
        if (buffer == 0) return GL_FALSE;
        return MG_State::pGLContext->ValidateBufferObject(buffer) ? GL_TRUE : GL_FALSE;
    }

    void BindBufferBase_State(GLenum target, GLuint pointIndex, GLuint buffer) {
        MGLOG_D("%s: target = %s, pointIndex = %u, buffer = %u", __func__,
                MG_Util::ConvertGLEnumToString(target).c_str(), pointIndex, buffer);
        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
        if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, pointIndex)) return;
        if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Transform feedback buffer bindings cannot change while transform "
                                             "feedback is active."));
            return;
        }
        MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, pointIndex);

        auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, pointIndex);
        SharedPtr<MG_State::GLState::BufferObject> bufferObject;
        if (buffer == 0) {
            point.Bind(nullptr);
            point.SetRange(Range1D(0, 0));
            GetBufferBindingSlot(bufferTarget).Bind(nullptr);
            return;
        }

        if (!BufferImpl::ValidateBufferName(buffer, true)) return;

        Bool doesBufferObjectCreated = MG_State::pGLContext->ValidateBufferObject(buffer);
        if (!doesBufferObjectCreated) {
            MG_State::pGLContext->CreateBufferObject(buffer);
        }
        bufferObject = MG_State::pGLContext->GetBufferObject(buffer);

        point.Bind(bufferObject);
        if (bufferObject) {
            point.SetRange(Range1D(0, bufferObject->GetSize()), false);
            MGLOG_D("%s: set range (0, %d)", __func__, bufferObject->GetSize());
        } else {
            point.ClearRange();
        }
        // The indexed bind also binds to the generic binding point of the same target
        // (GL 4.6 core 6.1.1). Callers rely on it: the texture_gather tests set up their
        // SSBO with BindBufferBase and then size it through glBufferData on the generic
        // target alone, which would otherwise raise GL_INVALID_OPERATION and leave the
        // buffer with no storage.
        GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
    }

    // GL 4.6 core 6.1.1: the constraints glBindBufferRange puts on the (offset, size) pair.
    // Every one of them is INVALID_VALUE, and all of them are checked before a single piece
    // of state is written - a rejected bind must leave the binding point exactly as it was.
    // They apply only to a non-zero buffer: buffer 0 detaches the binding point and ignores
    // offset and size, which is also how glBindBuffersRange spells "reset this element"
    // (a NULL buffers array, or a zero entry inside one).
    static Bool ValidateBufferRangeOffsetAndSize(GLenum target, GLintptr offset, GLsizeiptr size,
                                                 const char* funcName, Bool hasBuffer = true) {
        if (hasBuffer && size <= 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
                                             std::format("size ({}) must be greater than zero.", size)));
            return false;
        }
        if (offset < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
                                             std::format("offset ({}) must not be negative.", offset)));
            return false;
        }
        // GL_UNIFORM_BUFFER and GL_SHADER_STORAGE_BUFFER each constrain the offset to their own
        // implementation-defined alignment, which glGetIntegerv already answers.
        GLenum alignmentQuery = GL_NONE;
        if (target == GL_SHADER_STORAGE_BUFFER) {
            alignmentQuery = GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT;
        } else if (target == GL_UNIFORM_BUFFER) {
            alignmentQuery = GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT;
        }
        if (alignmentQuery != GL_NONE) {
            GLint alignment = 0;
            GetIntegerv(alignmentQuery, &alignment);
            if (alignment > 0 && (offset % static_cast<GLintptr>(alignment)) != 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", funcName,
                        std::format("offset ({}) must be a multiple of {} ({}).", offset,
                                    MG_Util::ConvertGLEnumToString(alignmentQuery), alignment)));
                return false;
            }
        }
        // GL 4.6 core 6.1.1 constrains the OFFSET to a multiple of four for both
        // TRANSFORM_FEEDBACK_BUFFER and ATOMIC_COUNTER_BUFFER (the atomic-counter one has no
        // queryable alignment pname, which is why it was missing here), and the SIZE only for
        // transform feedback, whose capture is written in whole 32-bit components. Extending the
        // size rule to atomic counters as well breaks a legal bind: the conformance suite splits
        // MAX_ATOMIC_COUNTER_BUFFER_SIZE evenly across the binding points and that quotient is
        // not required to land on four.
        if ((target == GL_TRANSFORM_FEEDBACK_BUFFER || target == GL_ATOMIC_COUNTER_BUFFER) && (offset % 4) != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
                                             std::format("offset ({}) must be a multiple of 4 for {}.", offset,
                                                         MG_Util::ConvertGLEnumToString(target))));
            return false;
        }
        if (target == GL_TRANSFORM_FEEDBACK_BUFFER && hasBuffer && (size % 4) != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", funcName,
                    std::format("size ({}) must be a multiple of 4 for GL_TRANSFORM_FEEDBACK_BUFFER.", size)));
            return false;
        }
        return true;
    }

    void BindBufferRange_State(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
        MGLOG_D("%s: target = %s, index = %u, buffer = %u, offset = %d, size = %d", __func__,
                MG_Util::ConvertGLEnumToString(target).c_str(), index, buffer, offset, size);
        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return;
        if (!BufferImpl::ValidateBufferBindingPointIndex(bufferTarget, index)) return;
        // The target's alignment rules are a property of the BINDING POINT, not of the buffer,
        // so they apply even when buffer is zero - which is exactly how
        // KHR-GL43.shader_storage_buffer_object.negative-api-bind probes the SSBO alignment
        // (glBindBufferRange(SHADER_STORAGE_BUFFER, 0, 0, alignment - 1, 0)). Only the size
        // rules need a buffer, since buffer 0 detaches the binding point and ignores size.
        if (!ValidateBufferRangeOffsetAndSize(target, offset, size, __func__, /*hasBuffer: */ buffer != 0)) return;
        if (bufferTarget == BufferTarget::TransformFeedback && MG_State::pGLContext->IsTransformFeedbackActive()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Transform feedback buffer bindings cannot change while transform "
                                             "feedback is active."));
            return;
        }
        MG_State::pGLContext->TouchBufferBindingPoint(bufferTarget, index);

        auto& point = MG_State::pGLContext->GetBufferBindingPoint(bufferTarget, index);
        SharedPtr<MG_State::GLState::BufferObject> bufferObject;
        if (buffer == 0) {
            point.Bind(nullptr);
            point.SetRange(Range1D(0, 0));
            GetBufferBindingSlot(bufferTarget).Bind(nullptr);
            return;
        }

        if (!BufferImpl::ValidateBufferName(buffer, true)) return;

        Bool doesBufferObjectCreated = MG_State::pGLContext->ValidateBufferObject(buffer);
        if (!doesBufferObjectCreated) {
            MG_State::pGLContext->CreateBufferObject(buffer);
        }
        bufferObject = MG_State::pGLContext->GetBufferObject(buffer);

        point.Bind(bufferObject);
        if (bufferObject) {
            point.SetRange(Range1D(offset, offset + size));
        } else {
            point.ClearRange();
        }
        // Also the generic binding point, exactly as BindBufferBase (GL 4.6 core 6.1.1).
        GetBufferBindingSlot(bufferTarget).Bind(bufferObject);
    }

    /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
    void GetBufferParameteriv(GLenum target, GLenum pname, GLint* params) {
        GetBufferParameteriv_State(target, pname, params);
    }

    void GetBufferPointerv(GLenum target, GLenum pname, void** params) {
        GetBufferPointerv_State(target, pname, params);
    }
    void GetBufferParameteri64v(GLenum target, GLenum pname, GLint64* params) {
        GetBufferParameteri64v_State(target, pname, params);
    }
    GLboolean IsBuffer(GLuint buffer) {
        return IsBuffer_State(buffer);
    }

    void DeleteBuffers(GLsizei n, const GLuint* buffers) {
        DeleteBuffers_State(n, buffers);
    }

    void FlushMappedBufferRange(GLenum target, GLintptr offset, GLsizeiptr length) {
        FlushMappedBufferRange_State(target, offset, length);
    }

    GLboolean UnmapBuffer(GLenum target) {
        return UnmapBuffer_State(target);
    }

    void* MapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) {
        return MapBufferRange_State(target, offset, length, access);
    }

    void* MapBuffer(GLenum target, GLenum access) {
        return MapBuffer_State(target, access);
    }

    void BufferStorage(GLenum target, GLsizeiptr size, const void* data, GLbitfield flags) {
        BufferStorage_State(target, size, data, flags);
    }

    void NamedBufferStorage(GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags) {
        NamedBufferStorage_State(buffer, size, data, flags);
    }

    void CreateBuffers(GLsizei n, GLuint* buffers) {
        CreateBuffers_State(n, buffers);
    }

    void NamedBufferData(GLuint buffer, GLsizeiptr size, const void* data, GLenum usage) {
        NamedBufferData_State(buffer, size, data, usage);
    }

    void NamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data) {
        NamedBufferSubData_State(buffer, offset, size, data);
    }

    void CopyNamedBufferSubData(GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset,
                                GLsizeiptr size) {
        CopyNamedBufferSubData_State(readBuffer, writeBuffer, readOffset, writeOffset, size);
    }

    void ClearBufferData(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void* data) {
        ClearBufferData_State(target, internalformat, format, type, data);
    }

    void ClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
                            GLenum type, const void* data) {
        ClearBufferSubData_State(target, internalformat, offset, size, format, type, data);
    }

    void ClearNamedBufferData(GLuint buffer, GLenum internalformat, GLenum format, GLenum type, const void* data) {
        ClearNamedBufferData_State(buffer, internalformat, format, type, data);
    }

    void ClearNamedBufferSubData(GLuint buffer, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format,
                                 GLenum type, const void* data) {
        ClearNamedBufferSubData_State(buffer, internalformat, offset, size, format, type, data);
    }

    void* MapNamedBuffer(GLuint buffer, GLenum access) {
        return MapNamedBuffer_State(buffer, access);
    }

    void* MapNamedBufferRange(GLuint buffer, GLintptr offset, GLsizeiptr length, GLbitfield access) {
        return MapNamedBufferRange_State(buffer, offset, length, access);
    }

    GLboolean UnmapNamedBuffer(GLuint buffer) {
        return UnmapNamedBuffer_State(buffer);
    }

    void FlushMappedNamedBufferRange(GLuint buffer, GLintptr offset, GLsizeiptr length) {
        FlushMappedNamedBufferRange_State(buffer, offset, length);
    }

    void GetNamedBufferParameteriv(GLuint buffer, GLenum pname, GLint* params) {
        GetNamedBufferParameteriv_State(buffer, pname, params);
    }

    void GetNamedBufferParameteri64v(GLuint buffer, GLenum pname, GLint64* params) {
        GetNamedBufferParameteri64v_State(buffer, pname, params);
    }

    void GetNamedBufferPointerv(GLuint buffer, GLenum pname, void** params) {
        GetNamedBufferPointerv_State(buffer, pname, params);
    }

    // FIXME: this should be a "backend" function
    void CopyBufferSubData(GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset,
                           GLsizeiptr size) {
        CopyBufferSubData_State(readTarget, writeTarget, readOffset, writeOffset, size);
    }

    void BufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void* data) {
        BufferSubData_State(target, offset, size, data);
    }

    void GetNamedBufferSubData(GLuint buffer, GLintptr offset, GLsizeiptr size, void* data) {
        GetNamedBufferSubData_State(buffer, offset, size, data);
    }

    void GetBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, void* data) {
        GetBufferSubData_State(target, offset, size, data);
    }

    void BufferData(GLenum target, GLsizeiptr size, const void* data, GLenum usage) {
        BufferData_State(target, size, data, usage);
    }

    void BindBuffer(GLenum target, GLuint buffer) {
        BindBuffer_State(target, buffer);
    }

    void GenBuffers(GLsizei n, GLuint* buffers) {
        GenBuffers_State(n, buffers);
    }

    void BindBufferBase(GLenum target, GLuint index, GLuint buffer) {
        BindBufferBase_State(target, index, buffer);
    }

    void BindBufferRange(GLenum target, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
        BindBufferRange_State(target, index, buffer, offset, size);
    }

    // ARB_multi_bind: defined by the spec as equivalent to a loop over the single-bind entry
    // points (with buffer 0 resetting the binding point) - but only AFTER an up-front check
    // of the whole [first, first + count) range. Looping straight into the single-bind entry
    // points reports the single-bind INVALID_VALUE for an out-of-range index instead of the
    // multi-bind INVALID_OPERATION, and binds the in-range prefix before failing.
    static Bool ValidateMultiBindBufferRange(GLenum target, GLuint first, GLsizei count, const char* funcName) {
        BufferTarget bufferTarget = MG_Util::ConvertGLEnumToBufferTarget(target);
        if (!BufferImpl::ValidateBufferBindingPointTarget(bufferTarget)) return false;
        return BufferImpl::ValidateBufferBindingPointRange(bufferTarget, first, count, funcName);
    }

    // ARB_multi_bind states the equivalence to a loop of single binds "except that ... buffers
    // will not be created if they do not exist": glBindBuffer instantiates a name glGenBuffers
    // merely reserved, glBindBuffers* must refuse it and raise INVALID_OPERATION instead
    // (KHR-GL44.multi_bind.errors_bind_buffers).
    //
    // Deliberately PER ELEMENT, not all-or-nothing: the equivalence the extension defines is a
    // loop, so a bad entry costs its own binding point and nothing else. Rejecting the whole
    // call instead cost multi_bind.functional_bind_buffers_base its bindings.
    static Bool IsExistingBufferForMultiBind(GLuint buffer, GLsizei index, const char* funcName) {
        if (buffer == 0 || MG_State::pGLContext->ValidateBufferObject(buffer)) return true;
        MG_State::pGLContext->RecordError(
            ErrorCode::InvalidOperation,
            MakeUnique<GenericErrorInfo>(
                "MG_Impl/GLImpl", funcName,
                std::format("buffers[{}] ({}) is not the name of an existing buffer object.", index, buffer)));
        return false;
    }

    void BindBuffersBase(GLenum target, GLuint first, GLsizei count, const GLuint* buffers) {
        if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
        for (GLsizei i = 0; i < count; ++i) {
            const GLuint buffer = buffers ? buffers[i] : 0;
            if (!IsExistingBufferForMultiBind(buffer, i, __func__)) continue;
            BindBufferBase_State(target, first + i, buffer);
        }
    }

    // The (offset, size) constraints are the one part of glBindBuffersRange that stays
    // per-element: ARB_multi_bind checks them separately for each binding point, leaves that
    // point unchanged on failure, and still applies the remaining elements - which is exactly
    // what looping into BindBufferRange_State does. Only the [first, first + count) range is
    // an up-front, all-or-nothing check. Elements that name buffer 0 (or a NULL buffers array)
    // reset the binding point through BindBufferBase_State and carry no offset/size to check.
    void BindBuffersRange(GLenum target, GLuint first, GLsizei count, const GLuint* buffers, const GLintptr* offsets,
                          const GLsizeiptr* sizes) {
        if (!ValidateMultiBindBufferRange(target, first, count, __func__)) return;
        for (GLsizei i = 0; i < count; ++i) {
            if (buffers && !IsExistingBufferForMultiBind(buffers[i], i, __func__)) continue;
            if (!buffers || buffers[i] == 0) {
                BindBufferBase_State(target, first + i, 0);
            } else {
                BindBufferRange_State(target, first + i, buffers[i], offsets ? offsets[i] : 0, sizes ? sizes[i] : 0);
            }
        }
    }
} // namespace MobileGL::MG_Impl::GLImpl
