// MobileGL - MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.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_Drawing.h"
#include <Config.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Backend/BackendObjects.h>
#include "../Getter/GL_Getter.h"

namespace MobileGL::MG_Impl::GLImpl {
    static Bool ValidateProgramForExecution(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
                                            const char* functionName) {
        if (!currentProgram) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "There is no current program object."));
            return false;
        }

        if (!currentProgram->GetLinkStatus()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             "The current program object is not linked."));
            return false;
        }

        return true;
    }

    // Takes the ALREADY-RESOLVED draw program rather than looking it up: GLContext::GetProgramForDraw
    // is not a plain getter (it settles the program's link and SPIR-V jobs so every version a
    // backend samples during this draw describes the program it is drawing), so the draw funnel
    // below resolves it exactly once and hands it to both users.
    static Bool ValidateResolvedProgramForDraw(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
                                               const char* functionName) {
        // "If there is no current program object or bound program pipeline object, the results of
        // a draw are UNDEFINED" - and undefined is not an error (GL 4.6 core 7.3, ES 3.1 7.3).
        // The draw is dropped, silently, which is one of the shapes "undefined" is allowed to
        // take; recording INVALID_OPERATION here is not, and es31cSeparateShaderObjsTests'
        // StateInteraction reads exactly that error back after useProgram(0) + bindProgramPipeline(0).
        // A DISPATCH is the opposite rule ("INVALID_OPERATION if there is no active program for
        // the compute shader stage"), which is why this lives on the draw path and not in the
        // shared ValidateProgramForExecution below.
        if (!currentProgram) return false;
        if (!ValidateProgramForExecution(currentProgram, functionName)) return false;

        // GL 4.6 core 7.4.1, the pipeline validation rule every vertex-transferring command
        // inherits: it is an INVALID_OPERATION when a tessellation control, tessellation
        // evaluation or geometry stage has an executable but no program supplies an executable
        // VERTEX shader. A non-separable program cannot reach this - the link rule forbids the
        // shape - so in practice it catches a program pipeline assembled out of stage programs,
        // which today draws happily and renders nothing.
        //
        // Asked of the EXECUTABLE, like the compute check below: for a pipeline the resolved
        // program is the graphics composite, whose linked-shader snapshot is built out of exactly
        // the pipeline's own graphics stage programs (GLContext::GetProgramForDraw), and the only
        // stage compositing ever invents is a default FRAGMENT shader. A fragment-only pipeline is
        // deliberately NOT rejected: the rule above names the three pre-rasterization stages, and
        // nothing else here should start refusing draws GL accepts.
        //
        // On the DRAW path only, never in ValidateProgramForExecution itself, so a dispatch -
        // which shares that helper and legitimately has no vertex stage - is untouched.
        const Bool hasPreRasterizationStage = currentProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
                                              currentProgram->HasLinkedShaderStage(ShaderStage::TessControl) ||
                                              currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
        if (hasPreRasterizationStage && !currentProgram->HasLinkedShaderStage(ShaderStage::Vertex)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", functionName,
                    "The program in use runs a geometry or tessellation stage but has no vertex shader stage."));
            return false;
        }

        return true;
    }

    // gl_NumSamples has no SPIR-V built-in, so the source pipeline lowers it onto a reserved
    // default-block uniform (see InjectNumSamplesBuiltinShim). This is where that uniform is paid
    // for: the value is a property of the DRAW FRAMEBUFFER, not of the program, so one program
    // drawn into a 4x target and then into the default framebuffer must see 4 and then 1 - which
    // rules out baking it at link time.
    //
    // Per draw rather than on framebuffer changes because the pair (program, framebuffer) is what
    // decides the value and either half can move between draws. It costs a phase-A flag read for
    // every program that has no shim, and a 4-byte compare for the ones that do: the write only
    // bumps the UBO content version when the number actually changes, so a run of draws into one
    // framebuffer re-uploads nothing.
    static void PublishDrawFramebufferSampleCount(const SharedPtr<MG_State::GLState::ProgramObject>& program) {
        if (!program || !program->UsesReservedNumSamples()) return;
        // GL 4.6 core 15.2.2: gl_NumSamples is the number of samples in the framebuffer, or ONE
        // when the target is not multisampled - where glGetIntegerv(GL_SAMPLES) answers zero.
        program->WriteReservedNumSamples(static_cast<Int>(std::max<GLint>(ResolveDrawFramebufferSampleCount(), 1)));
    }

    // The one funnel every drawing command passes through. Order is load-bearing: validate first
    // (a rejected draw must leave state alone), then publish the sample count - which reads the
    // DRAW FRAMEBUFFER binding, so it has to run after the caller's framebuffer state is settled
    // and before the backend consumes the program's UBO content version.
    static Bool PrepareCurrentProgramForDraw(const char* functionName) {
        const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
        if (!ValidateResolvedProgramForDraw(currentProgram, functionName)) return false;
        PublishDrawFramebufferSampleCount(currentProgram);
        return true;
    }

    // A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound
    // that is the pipeline's compute stage program, not the graphics composite a draw would
    // build - which no longer contains a compute stage to find at all.
    static Bool ValidateCurrentProgramForCompute(const char* functionName) {
        const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch();
        if (!ValidateProgramForExecution(currentProgram, functionName)) return false;

        // Of the EXECUTABLE, not the live attach list: attaching a compute shader to an
        // already-linked graphics program does not give that program a compute stage to
        // dispatch (GL 4.6 core 7.3), and letting the dispatch through on the strength of the
        // attach hands the backend a program whose SPIR-V has no compute module in it.
        if (!currentProgram->HasLinkedShaderStage(ShaderStage::Compute)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             "The current program object has no compute shader stage."));
            return false;
        }

        return true;
    }

    // Primitives a draw of `count` vertices in `mode` assembles (0 for
    // incomplete primitives). Used for the CPU-side transform feedback
    // primitive accounting.
    static Uint64 CountPrimitivesForDraw(GLenum mode, GLsizei count) {
        if (count <= 0) return 0;
        switch (mode) {
        case GL_POINTS: return static_cast<Uint64>(count);
        case GL_LINES: return static_cast<Uint64>(count / 2);
        case GL_LINE_STRIP: return count >= 2 ? static_cast<Uint64>(count - 1) : 0;
        case GL_LINE_LOOP: return count >= 2 ? static_cast<Uint64>(count) : 0;
        case GL_TRIANGLES: return static_cast<Uint64>(count / 3);
        case GL_TRIANGLE_STRIP:
        case GL_TRIANGLE_FAN: return count >= 3 ? static_cast<Uint64>(count - 2) : 0;
        // Adjacency primitives (GL 4.6 core table 10.1). Only a geometry stage can consume
        // them, and it is the ADJACENT-free primitive count that reaches it: 4 vertices per
        // line, 6 per triangle, one per step for the strips. Answering 0 here - which is what
        // the default arm did - made AccountTransformFeedbackPrimitives bail before it had
        // recorded anything, so an adjacency capture advanced neither the captured-vertex
        // counter the scattered-capture path is bounded by nor the geometry-capture-draw flag
        // that routes the transform feedback queries to the driver's own counter.
        case GL_LINES_ADJACENCY: return static_cast<Uint64>(count / 4);
        case GL_LINE_STRIP_ADJACENCY: return count >= 4 ? static_cast<Uint64>(count - 3) : 0;
        case GL_TRIANGLES_ADJACENCY: return static_cast<Uint64>(count / 6);
        case GL_TRIANGLE_STRIP_ADJACENCY: return count >= 6 ? static_cast<Uint64>((count - 4) / 2) : 0;
        // GL_PATCHES is deliberately absent: the tessellator's amplification is not knowable
        // on the CPU, and answering 0 is what defers GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
        // to the driver's own counter, which is the only correct source for a patch capture.
        default: return 0;
        }
    }

    // Accumulate the transform feedback primitive counter for a captured draw.
    // Draws without a geometry stage write exactly the primitives they assemble,
    // clamped by the capture buffers' remaining capacity (a full buffer stops
    // recording whole primitives, which is what PRIMITIVES_WRITTEN reports).
    // Geometry amplification is not modelled here.
    static void AccountTransformFeedbackPrimitives(GLenum mode, GLsizei count) {
        if (!MG_State::pGLContext->IsTransformFeedbackActive()) return;
        // A paused span captures nothing, so a draw made while paused contributes to
        // PRIMITIVES_GENERATED but not to TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN.
        if (MG_State::pGLContext->IsTransformFeedbackPaused()) {
            MG_State::pGLContext->AddTransformFeedbackPausedPrimitives(CountPrimitivesForDraw(mode, count));
            return;
        }
        Uint64 primitives = CountPrimitivesForDraw(mode, count);
        if (primitives == 0) return;
        MG_State::pGLContext->AddTransformFeedbackInputPrimitives(primitives);

        Uint64 verticesPerPrimitive = 1;
        switch (mode) {
        case GL_LINES:
        case GL_LINE_STRIP:
        case GL_LINE_LOOP:
        // An adjacency primitive delivers the same line/triangle to the geometry stage; the
        // adjacent vertices are context, not part of the primitive.
        case GL_LINES_ADJACENCY:
        case GL_LINE_STRIP_ADJACENCY:
            verticesPerPrimitive = 2;
            break;
        case GL_TRIANGLES:
        case GL_TRIANGLE_STRIP:
        case GL_TRIANGLE_FAN:
        case GL_TRIANGLES_ADJACENCY:
        case GL_TRIANGLE_STRIP_ADJACENCY:
            verticesPerPrimitive = 3;
            break;
        default:
            break;
        }

        const auto& program = MG_State::pGLContext->GetTransformFeedbackProgram();
        if (program != nullptr) {
            // A geometry stage writes what it emits, not what the draw assembled, and the
            // amplification factor lives in the shader. Record that this span contained such
            // a draw so the transform feedback queries keep their backend result for it.
            if (program->HasLinkedShaderStage(ShaderStage::Geometry)) {
                MG_State::pGLContext->AddTransformFeedbackGeometryCaptureDraw();
            }
            // Capacity in captured vertices = the tightest bound buffer.
            Uint64 capacityVertices = ~0ull;
            for (SizeT i = 0; i < program->GetTransformFeedbackBufferCount(); ++i) {
                const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(i));
                if (stride == 0) continue;
                const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
                                                                                static_cast<Uint>(i));
                const Range1D range = point.GetRange();
                const Uint64 bytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
                capacityVertices = std::min<Uint64>(capacityVertices, bytes / stride);
            }
            if (capacityVertices != ~0ull) {
                const Uint64 usedVertices = MG_State::pGLContext->GetTransformFeedbackCapturedVertices();
                const Uint64 remainingVertices = capacityVertices > usedVertices ? capacityVertices - usedVertices : 0;
                primitives = std::min<Uint64>(primitives, remainingVertices / verticesPerPrimitive);
            }
        }
        MG_State::pGLContext->AddTransformFeedbackPrimitives(primitives);
        MG_State::pGLContext->AddTransformFeedbackCapturedVertices(primitives * verticesPerPrimitive);
        // Only draws that get this far are in the written counter at all. The instanced and
        // indirect entry points never call this function, so a span that contains one is NOT
        // fully accounted, and the queries must be able to tell: they compare this counter's
        // delta against zero before standing in for the backend's own result.
        MG_State::pGLContext->AddTransformFeedbackAccountedCaptureDraw();
    }

    // Every primitive mode a draw command accepts (GL 4.6 core table 10.1, plus
    // GL_PATCHES for the tessellation pipeline). Anything else is GL_INVALID_ENUM.
    static Bool IsAcceptedPrimitiveMode(GLenum mode) {
        switch (mode) {
        case GL_POINTS:
        case GL_LINES:
        case GL_LINE_LOOP:
        case GL_LINE_STRIP:
        case GL_LINES_ADJACENCY:
        case GL_LINE_STRIP_ADJACENCY:
        case GL_TRIANGLES:
        case GL_TRIANGLE_STRIP:
        case GL_TRIANGLE_FAN:
        case GL_TRIANGLES_ADJACENCY:
        case GL_TRIANGLE_STRIP_ADJACENCY:
        case GL_PATCHES:
            return true;
        default:
            return false;
        }
    }

    // The `mode` INVALID_ENUM in isolation, so a draw entry point can raise it BEFORE any of the
    // state-dependent INVALID_OPERATIONs below. GL 4.6 core 10.4 makes a bad mode INVALID_ENUM
    // unconditionally, while "no current program" is not even a spec-listed draw error - it is
    // MobileGL's own null-dereference guard - so it must never shadow the enum check
    // (KHR-GL31.api.coverage calls glDrawArraysInstanced/glDrawElementsInstanced with mode
    // GL_POINTS-1 against a bare context and pins GL_INVALID_ENUM).
    static Bool ValidatePrimitiveModeEnum(const char* functionName, GLenum mode) {
        if (IsAcceptedPrimitiveMode(mode)) return true;

        MG_State::pGLContext->RecordError(
            ErrorCode::InvalidEnum,
            MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "mode is not an accepted primitive type."));
        return false;
    }

    static Bool ValidatePrimitiveModeForBackend(const char* functionName, GLenum mode) {
        if (!ValidatePrimitiveModeEnum(functionName, mode)) {
            return false;
        }

        const auto& activeBackendObject = MG_Backend::pActiveBackendObject;
        if (!activeBackendObject) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "No active backend object."));
            return false;
        }

        const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
        if (vao && vao->GetExternalIndex() == 0 && !MG_State::IsRelaxedSemanticsActive()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             "Default vertex array object cannot be used for drawing in core profile."));
            return false;
        }

        const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();

        // GL 4.6 core 10.1: the tessellation pipeline's only input primitive is GL_PATCHES, and
        // GL_PATCHES has no meaning without it. Both directions are INVALID_OPERATION, and
        // neither was implemented - which is two of the four sites
        // KHR-GL43.transform_feedback.api_errors_test checks with one shared message string.
        // The EVALUATION stage is what decides: a control stage cannot run without one, and a
        // program carrying only an evaluation stage still tessellates, through GL's
        // fixed-function pass-through control stage (11.2.2).
        // Asked of the LAST LINK, not the live attach list (GL 4.6 core 7.3): attaching a
        // tessellation evaluation shader to an already-linked program does not put it in the
        // executable, so reading the live list here would reject every non-GL_PATCHES draw
        // against a program that does not tessellate - and keep rejecting them, since a detach
        // is likewise deferred to the next link.
        const Bool tessellationActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::TessEval);
        if (tessellationActive && mode != GL_PATCHES) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", functionName,
                    "A program with a tessellation evaluation shader can only be drawn with GL_PATCHES."));
            return false;
        }
        if (!tessellationActive && mode == GL_PATCHES) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             "GL_PATCHES requires an active tessellation evaluation shader."));
            return false;
        }

        // A geometry stage only accepts the primitive types that decompose into its declared
        // input primitive (GL 4.6 core 11.3.1); anything else is INVALID_OPERATION. GL_PATCHES
        // is the tessellation pipeline's input and reaches the geometry stage already
        // converted, so it is not constrained here.
        //
        // "Is there a geometry stage at all" has to be asked of the STAGE, never of the input
        // primitive: GL_NONE and GL_POINTS are both 0, so a `layout(points) in` geometry shader
        // is indistinguishable from no geometry shader by its reflected input type alone. The
        // sentinel test this replaces therefore skipped the whole rule for exactly the geometry
        // shaders whose input is the most restrictive one - every mode but GL_POINTS was
        // accepted (KHR-GL43.transform_feedback.api_errors_test draws a points-in geometry
        // program with GL_LINES and requires INVALID_OPERATION).
        //
        // And it has to be asked of the LAST LINK: gsInputPrimitive is a link artifact, so
        // pairing it with the live attach list would re-point the very same 0-aliasing rather
        // than remove it. In the window after glAttachShader(GS) on a linked program the live
        // list says "geometry present" while the artifact still reads GL_NONE == GL_POINTS, and
        // the switch below would silently reject every mode but GL_POINTS.
        const Bool geometryActive = currentProgram && currentProgram->HasLinkedShaderStage(ShaderStage::Geometry);
        const GLenum gsInput = geometryActive ? currentProgram->GetGeometryInputType() : GL_NONE;
        if (geometryActive && mode != GL_PATCHES) {
            Bool compatible = false;
            switch (gsInput) {
            case GL_POINTS:
                compatible = mode == GL_POINTS;
                break;
            case GL_LINES:
                compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP;
                break;
            case GL_LINES_ADJACENCY:
                compatible = mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
                break;
            case GL_TRIANGLES:
                compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN;
                break;
            case GL_TRIANGLES_ADJACENCY:
                compatible = mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
                break;
            default:
                break;
            }
            if (!compatible) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", functionName,
                        "Primitive mode is incompatible with the geometry shader's input primitive type."));
                return false;
            }
        }

        // While transform feedback is active the draw's primitive type must match
        // the feedback primitive mode (GL 3.3 core 13.2.2). With a geometry shader
        // the constraint moves to the shader's output primitive type instead, so
        // the draw mode itself is unconstrained here - and a TESSELLATION EVALUATION
        // stage relocates it exactly the same way (GL 4.6 core 13.2.2 names both):
        // what is captured is the tessellator's output primitive, and the draw mode
        // can only ever be GL_PATCHES. A paused span is exempt: it captures nothing,
        // so there is nothing for the mode to be incompatible with (GL 4.6 core 13.2.3).
        const auto& feedbackProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
        // Both stage tests are asked of the last link, for the same reason as the two guards
        // above: what relocates the constraint is a stage the program actually RUNS, and an
        // attach that has not been linked in yet gives it none.
        const Bool feedbackModeIsProgramDriven =
            feedbackProgram && (feedbackProgram->HasLinkedShaderStage(ShaderStage::Geometry) ||
                                feedbackProgram->HasLinkedShaderStage(ShaderStage::TessEval));
        if (MG_State::pGLContext->IsTransformFeedbackActive() &&
            !MG_State::pGLContext->IsTransformFeedbackPaused() && !feedbackModeIsProgramDriven) {
            const GLenum feedbackMode = MG_State::pGLContext->GetTransformFeedbackPrimitiveMode();
            Bool compatible = false;
            switch (feedbackMode) {
            case GL_POINTS:
                compatible = mode == GL_POINTS;
                break;
            // The adjacency modes belong here too (GL 4.6 core table 13.1, ES 3.2 table 12.1).
            // This arm is only reached when the program has NO geometry or tessellation
            // evaluation stage, and without a geometry stage the adjacent vertices are simply
            // ignored (GL 4.6 core 10.1) - the primitive assembled IS a plain line or triangle,
            // so the combination is legal and must capture. Omitting them raised a spurious
            // GL_INVALID_OPERATION and dropped the draw entirely, leaving the capture buffer
            // with its pre-draw bytes. The geometry-stage input table above already carries the
            // same four arms; this is the second table catching up with it.
            case GL_LINES:
                compatible = mode == GL_LINES || mode == GL_LINE_STRIP || mode == GL_LINE_LOOP ||
                             mode == GL_LINES_ADJACENCY || mode == GL_LINE_STRIP_ADJACENCY;
                break;
            case GL_TRIANGLES:
                compatible = mode == GL_TRIANGLES || mode == GL_TRIANGLE_STRIP || mode == GL_TRIANGLE_FAN ||
                             mode == GL_TRIANGLES_ADJACENCY || mode == GL_TRIANGLE_STRIP_ADJACENCY;
                break;
            default:
                break;
            }
            if (!compatible) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", functionName,
                        "Primitive mode is incompatible with the active transform feedback primitive mode."));
                return false;
            }
        }

        return true;
    }

    // Byte size of the command structures the indirect draws read (GL 4.6 core 10.3.10).
    constexpr SizeT kDrawArraysIndirectCommandBytes = 4 * sizeof(Uint32);
    constexpr SizeT kDrawElementsIndirectCommandBytes = 5 * sizeof(Uint32);

    // Shared preconditions of every *Indirect draw: `indirect` is a byte offset into the
    // buffer bound to GL_DRAW_INDIRECT_BUFFER, must be 4-byte aligned, and the whole
    // command has to lie inside that buffer.
    static Bool ValidateIndirectDrawSource(const char* functionName, const void* indirect, SizeT commandBytes) {
        const auto offset = reinterpret_cast<uintptr_t>(indirect);
        if (offset % 4 != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             "indirect offset must be a multiple of 4."));
            return false;
        }

        const auto& buffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
        if (!buffer) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             "No buffer is bound to GL_DRAW_INDIRECT_BUFFER."));
            return false;
        }

        if (offset + commandBytes > buffer->GetSize()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             "The indirect command extends past the end of the bound "
                                             "GL_DRAW_INDIRECT_BUFFER."));
            return false;
        }
        return true;
    }

    // Index type accepted by the DrawElements family (GL 4.6 core 10.3.9).
    static Bool ValidateDrawElementsIndexType(const char* functionName, GLenum type) {
        switch (type) {
        case GL_UNSIGNED_BYTE:
        case GL_UNSIGNED_SHORT:
        case GL_UNSIGNED_INT:
            return true;
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "type is not an accepted index type."));
            return false;
        }
    }

    // GL 4.6 core 10.3.9: every DrawElements-family count is a sizei and "if count is negative, an
    // INVALID_VALUE error is generated". The same sentence covers instancecount and the
    // MultiDraw* drawcount, so one helper serves all of them; the parameter is named for the
    // caller so the message says which argument the application actually got wrong.
    static Bool ValidateNonNegativeDrawArgument(const char* functionName, const char* argumentName, GLsizei value) {
        if (value >= 0) return true;
        MG_State::pGLContext->RecordError(
            ErrorCode::InvalidValue,
            MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                         String(argumentName) + " must be non-negative."));
        return false;
    }

    // GL 4.6 core 10.3.9 for DrawRangeElements*: "if end < start, an INVALID_VALUE error is
    // generated". Both are uints, so a caller that passes -1 for start arrives here as
    // 0xFFFFFFFF and is caught by the same comparison - which is exactly what
    // KHR-GL4x.draw_elements_base_vertex_tests.invalid_count_argument checks.
    static Bool ValidateDrawElementsRange(const char* functionName, GLuint start, GLuint end) {
        if (end >= start) return true;
        MG_State::pGLContext->RecordError(
            ErrorCode::InvalidValue,
            MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "end must not be less than start."));
        return false;
    }

    // GL 4.6 core 10.9: inside a conditional block whose predicate did not pass, the drawing
    // commands, Clear, ClearBuffer* and the compute dispatches are DISCARDED. The gate sits on the
    // wrappers that ISSUE the backend call rather than at the top of each entry point, so that
    // everything a real driver would still do inside the block - argument validation and the
    // errors it raises - happens exactly as it does outside one, and only the command itself is
    // dropped. It is deliberately not on the frontend's transform-feedback accounting either:
    // that mirrors what the capture stage would have written, and a conditional block around a
    // capturing draw has no test coverage in either direction.
    static Bool ConditionalRenderDiscardsCommand() {
        return MG_State::pGLContext->ConditionalRenderDiscardsCommands();
    }

    void Clear_Backend(GLbitfield mask) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.Clear(mask);
    }

    void DrawElements_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawElements(mode, count, type, indices);
    }

    void MultiDrawElements_Backend(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
                                   GLsizei drawcount) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.MultiDrawElements(mode, count, type, indices, drawcount);
    }

    void MultiDrawElementsBaseVertex_Backend(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
                                             GLsizei drawcount, const GLint* basevertex) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsBaseVertex(mode, count, type, indices, drawcount,
                                                                          basevertex);
    }

    void DrawArrays_Backend(GLenum mode, GLint first, GLsizei count) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawArrays(mode, first, count);
    }

    void MultiDrawArrays_Backend(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.MultiDrawArrays(mode, first, count, drawcount);
    }

    void DrawElementsBaseVertex_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices,
                                        GLint basevertex) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawElementsBaseVertex(mode, count, type, indices, basevertex);
    }

    void MultiDrawElementsIndirect_Backend(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
                                           GLsizei stride) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
    }

    void MultiDrawArraysIndirect_Backend(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirect(mode, indirect, drawcount, stride);
    }

    void MultiDrawElementsIndirectCount_Backend(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
                                                GLsizei maxdrawcount, GLsizei stride) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount(mode, type, indirect, drawcount,
                                                                             maxdrawcount, stride);
    }

    void MultiDrawArraysIndirectCount_Backend(GLenum mode, const void* indirect, GLintptr drawcount,
                                              GLsizei maxdrawcount, GLsizei stride) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount(mode, indirect, drawcount, maxdrawcount,
                                                                           stride);
    }

    void DrawRangeElementsBaseVertex_Backend(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
                                             const void* indices, GLint basevertex) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawRangeElementsBaseVertex(mode, start, end, count, type, indices,
                                                                          basevertex);
    }

    void DrawRangeElements_Backend(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
                                   const void* indices) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawRangeElements(mode, start, end, count, type, indices);
    }

    void DrawElementsInstancedBaseVertexBaseInstance_Backend(GLenum mode, GLsizei count, GLenum type,
                                                             const void* indices, GLsizei instancecount,
                                                             GLint basevertex, GLuint baseinstance) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertexBaseInstance(
            mode, count, type, indices, instancecount, basevertex, baseinstance);
    }

    void DrawElementsInstancedBaseVertex_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices,
                                                 GLsizei instancecount, GLint basevertex) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseVertex(mode, count, type, indices, instancecount,
                                                                              basevertex);
    }

    void DrawElementsInstancedBaseInstance_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices,
                                                   GLsizei instancecount, GLuint baseinstance) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstancedBaseInstance(mode, count, type, indices,
                                                                                instancecount, baseinstance);
    }

    void DrawElementsInstanced_Backend(GLenum mode, GLsizei count, GLenum type, const void* indices,
                                       GLsizei instancecount) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawElementsInstanced(mode, count, type, indices, instancecount);
    }

    void DrawElementsIndirect_Backend(GLenum mode, GLenum type, const void* indirect) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawElementsIndirect(mode, type, indirect);
    }
    void DrawArraysInstancedBaseInstance_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
                                                 GLuint baseinstance) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstancedBaseInstance(mode, first, count, instancecount,
                                                                              baseinstance);
    }

    void DrawArraysInstanced_Backend(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawArraysInstanced(mode, first, count, instancecount);
    }

    void DrawArraysIndirect_Backend(GLenum mode, const void* indirect) {
#ifdef TRACY_ENABLE
        ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
        if (ConditionalRenderDiscardsCommand()) return;
        MG_Backend::gBackendFunctionsTable.GL.DrawArraysIndirect(mode, indirect);
    }

    /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
    void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
        auto dispatchCompute = MG_Backend::gBackendFunctionsTable.GL.DispatchCompute;
        if (!dispatchCompute) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support compute dispatch."));
            return;
        }
        if (!ValidateCurrentProgramForCompute(__func__)) return;
        // GL 4.6 core 19: each num_groups_* must be within GL_MAX_COMPUTE_WORK_GROUP_COUNT
        // for its dimension. GetIntegeri_v already floors that at the spec minimum.
        const GLuint numGroups[3] = {numGroupsX, numGroupsY, numGroupsZ};
        for (GLuint dimension = 0; dimension < 3; ++dimension) {
            GLint maxGroups = 0;
            GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, dimension, &maxGroups);
            if (numGroups[dimension] > static_cast<GLuint>(std::max(maxGroups, 0))) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                                 "num_groups exceeds GL_MAX_COMPUTE_WORK_GROUP_COUNT for dimension " +
                                                     std::to_string(dimension) + "."));
                return;
            }
        }
        // GL 4.3 added both dispatches to the conditional-render set (GL 4.6 core 10.9), which is
        // exactly what KHR-GL43.compute_shader.conditional-dispatching checks.
        if (ConditionalRenderDiscardsCommand()) return;
        dispatchCompute(numGroupsX, numGroupsY, numGroupsZ);
    }

    void DispatchComputeIndirect(GLintptr indirect) {
        // Argument and binding validation runs FIRST. Both are properties of the call and of GL
        // state, so a context whose backend cannot dispatch at all must still report the
        // argument error the spec names rather than masking every one of them with
        // "unsupported" - which is what put GL_INVALID_OPERATION where
        // KHR-GL43.compute_shader.api-indirect expects GL_INVALID_VALUE.
        //
        // GL 4.6 core 19: `indirect` is a byte offset into GL_DISPATCH_INDIRECT_BUFFER -
        // negative or misaligned is INVALID_VALUE, nothing bound is INVALID_OPERATION.
        if (indirect < 0 || (indirect % 4) != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "indirect must be non-negative and a multiple of 4."));
            return;
        }
        const auto& indirectBuffer =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DispatchIndirect).GetBoundObject();
        if (!indirectBuffer) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "No buffer is bound to GL_DISPATCH_INDIRECT_BUFFER."));
            return;
        }
        // ...and the same INVALID_OPERATION covers "the command would source data beyond the end
        // of the bound buffer object" (GL 4.6 core 19): the dispatch reads three uints starting
        // at `indirect`.
        constexpr SizeT kDispatchIndirectCommandSize = 3 * sizeof(Uint32);
        if (static_cast<SizeT>(indirect) + kDispatchIndirectCommandSize > indirectBuffer->GetSize()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", __func__,
                    std::format("indirect ({}) + 12 bytes runs past the end of the {}-byte buffer bound to "
                                "GL_DISPATCH_INDIRECT_BUFFER.",
                                indirect, indirectBuffer->GetSize())));
            return;
        }
        auto dispatchComputeIndirect = MG_Backend::gBackendFunctionsTable.GL.DispatchComputeIndirect;
        if (!dispatchComputeIndirect) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Backend does not support indirect compute dispatch."));
            return;
        }
        if (!ValidateCurrentProgramForCompute(__func__)) return;
        if (ConditionalRenderDiscardsCommand()) return;
        dispatchComputeIndirect(indirect);
    }

    void PatchParameteri(GLenum pname, GLint value) {
        if (pname != GL_PATCH_VERTICES) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "pname must be GL_PATCH_VERTICES."));
            return;
        }
        GLint maxPatchVertices = 32;
        GetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);
        if (value <= 0 || value > maxPatchVertices) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "value must be in [1, GL_MAX_PATCH_VERTICES]."));
            return;
        }
        MG_State::pGLContext->SetPatchVertices(static_cast<Uint>(value));
        if (const auto patchParameteri = MG_Backend::gBackendFunctionsTable.GL.PatchParameteri) {
            patchParameteri(pname, value);
        }
    }

    // GL 4.6 core 11.2.2. The default tessellation levels a program with an evaluation stage and
    // NO control stage tessellates at; both backends have to synthesize that control stage
    // themselves (ES 3.2 and Vulkan both require one), and they compile these numbers into it, so
    // there is no backend entry point to forward to - ES has none at all. INVALID_ENUM on a bad
    // pname is the only error the spec lists: any float values are accepted, negatives and NaN
    // included, and it is the tessellator that clamps them.
    //
    // This used to be a stub, which is why the two synthesizers hardcoded 1.0.
    void PatchParameterfv(GLenum pname, const GLfloat* values) {
        if (pname != GL_PATCH_DEFAULT_OUTER_LEVEL && pname != GL_PATCH_DEFAULT_INNER_LEVEL) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", __func__,
                    "pname must be GL_PATCH_DEFAULT_OUTER_LEVEL or GL_PATCH_DEFAULT_INNER_LEVEL."));
            return;
        }
        if (!values) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "values pointer cannot be null"));
            return;
        }
        if (pname == GL_PATCH_DEFAULT_OUTER_LEVEL) {
            MG_State::pGLContext->SetPatchDefaultOuterLevel(
                FloatVec4(values[0], values[1], values[2], values[3]));
        } else {
            MG_State::pGLContext->SetPatchDefaultInnerLevel(FloatVec2(values[0], values[1]));
        }
    }

    namespace {
        // GL 4.6 core 7.11.2 (and ARB_shader_image_load_store, which introduced the call): the
        // barrier bitfield is INVALID_VALUE unless every bit is one of the defined ones, with
        // GL_ALL_BARRIER_BITS - which is 0xFFFFFFFF, not the union of the list - accepted whole.
        // Forwarding an undefined bit to the host driver let a caller that had computed its mask
        // wrongly (or reused an ES-only bit) get silence instead of the error the spec promises.
        constexpr GLbitfield kAllDefinedBarrierBits =
            GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT | GL_ELEMENT_ARRAY_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT |
            GL_TEXTURE_FETCH_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL_COMMAND_BARRIER_BIT |
            GL_PIXEL_BUFFER_BARRIER_BIT | GL_TEXTURE_UPDATE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT |
            GL_FRAMEBUFFER_BARRIER_BIT | GL_TRANSFORM_FEEDBACK_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT |
            GL_SHADER_STORAGE_BARRIER_BIT | GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT | GL_QUERY_BUFFER_BARRIER_BIT;

        Bool ValidateMemoryBarrierBits(const char* function, GLbitfield barriers) {
            if (barriers == GL_ALL_BARRIER_BITS) return true;
            if ((barriers & ~kAllDefinedBarrierBits) != 0) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", function,
                                                 "barriers contains bits that are not defined barrier bits."));
                return false;
            }
            return true;
        }
    } // namespace

    void MemoryBarrier(GLbitfield barriers) {
        if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
        auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
        if (!memoryBarrier) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
            return;
        }
        memoryBarrier(barriers);
    }

    void TextureBarrier() {
        // GL 4.5 core 8.26 / GL_ARB_texture_barrier: order every write the fixed-function
        // framebuffer has already issued ahead of every subsequent texture fetch, so a shader may
        // read texels of a texture that is also attached to the current framebuffer.
        //
        // Both backends serve this through their existing memory-barrier hook rather than a new
        // entry point of their own: GL_FRAMEBUFFER_BARRIER_BIT is the source half (framebuffer
        // writes) and GL_TEXTURE_FETCH_BARRIER_BIT the destination half (texture fetches), which
        // is exactly the dependency ARB_texture_barrier defines - just expressed with the wider
        // scope glMemoryBarrier gives it. That is a superset of the required ordering, never a
        // subset, so it cannot under-synchronize.
        auto memoryBarrier = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrier;
        if (!memoryBarrier) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Backend does not support memory barriers."));
            return;
        }
        memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
    }

    void MemoryBarrierByRegion(GLbitfield barriers) {
        if (!ValidateMemoryBarrierBits(__func__, barriers)) return;
        auto memoryBarrierByRegion = MG_Backend::gBackendFunctionsTable.GL.MemoryBarrierByRegion;
        if (!memoryBarrierByRegion) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Backend does not support regional memory barriers."));
            return;
        }
        memoryBarrierByRegion(barriers);
    }

    void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        MultiDrawElementsIndirect_Backend(mode, type, indirect, drawcount, stride);
    }

    void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        MultiDrawArraysIndirect_Backend(mode, indirect, drawcount, stride);
    }

    // ARB_indirect_parameters / GL 4.6 core 10.4: `drawcount` is a byte offset into the buffer
    // bound to PARAMETER_BUFFER and holds one uint draw count. Three errors have to be raised
    // before the call reaches a backend, and none of them was
    // (KHR-GL46.indirect_parameters_tests.MultiDraw{Arrays,Elements}IndirectCount):
    //   * drawcount not a multiple of four                                  INVALID_VALUE
    //   * nothing bound to PARAMETER_BUFFER, or the uint at `drawcount`
    //     lies past its end                                                 INVALID_OPERATION
    //   * maxdrawcount commands from `indirect` run past the end of the
    //     buffer bound to DRAW_INDIRECT_BUFFER                              INVALID_OPERATION
    static Bool ValidateIndirectCountDraw(GLintptr indirect, GLintptr drawcount, GLsizei maxdrawcount,
                                          GLsizei stride, SizeT commandSize, const char* funcName) {
        if (drawcount < 0 || (drawcount % 4) != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
                                             "drawcount must be non-negative and a multiple of four."));
            return false;
        }
        const auto& parameterBuffer =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
        if (!parameterBuffer ||
            static_cast<SizeT>(drawcount) + sizeof(Uint32) > parameterBuffer->GetSize()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
                                             "No buffer is bound to GL_PARAMETER_BUFFER, or drawcount runs past "
                                             "the end of the one that is."));
            return false;
        }
        if (maxdrawcount < 0 || stride < 0 || indirect < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
                                             "indirect, maxdrawcount and stride must all be non-negative."));
            return false;
        }
        const SizeT effectiveStride = stride != 0 ? static_cast<SizeT>(stride) : commandSize;
        const auto& indirectBuffer =
            MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
        // A zero maxdrawcount sources nothing, so it cannot run past anything.
        const SizeT requiredBytes =
            maxdrawcount == 0 ? 0
                              : static_cast<SizeT>(indirect) +
                                    static_cast<SizeT>(maxdrawcount - 1) * effectiveStride + commandSize;
        if (!indirectBuffer || requiredBytes > indirectBuffer->GetSize()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", funcName,
                                             "maxdrawcount commands would be sourced from beyond the end of the "
                                             "buffer bound to GL_DRAW_INDIRECT_BUFFER."));
            return false;
        }
        return true;
    }

    void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
                                        GLsizei maxdrawcount, GLsizei stride) {
        // Argument validation before the backend-availability check: see DispatchComputeIndirect.
        // DrawElementsIndirectCommand: count, instanceCount, firstIndex, baseVertex, baseInstance.
        if (!ValidateIndirectCountDraw(reinterpret_cast<GLintptr>(indirect), drawcount, maxdrawcount, stride,
                                       5 * sizeof(Uint32), __func__)) {
            return;
        }
        // The only two draw entry points that were missing this. Every backend draw path
        // dereferences GetProgramForDraw() unconditionally, so "no current program" has to be
        // stopped here or it is a null dereference rather than the INVALID_OPERATION the spec
        // asks for - reachable through a bound pipeline that supplies no graphics stage.
        //
        // AFTER the argument checks, unlike the sibling draw entry points, and deliberately:
        // the argument rules here are properties of the call rather than of GL state, and
        // NegativeApiErrorsTest.IndirectParameterDrawsCheckBothBuffers pins the INVALID_VALUE
        // they produce for a call made with no program bound. Same precedence decision, and
        // the same reason, as DispatchComputeIndirect above.
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount;
        if (!multiDrawElementsIndirectCount) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Backend does not support indirect-parameter indexed draws."));
            return;
        }
        MultiDrawElementsIndirectCount_Backend(mode, type, indirect, drawcount, maxdrawcount, stride);
    }

    void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
                                      GLsizei maxdrawcount, GLsizei stride) {
        // Argument validation before the backend-availability check: see DispatchComputeIndirect.
        // DrawArraysIndirectCommand: count, instanceCount, first, baseInstance.
        if (!ValidateIndirectCountDraw(reinterpret_cast<GLintptr>(indirect), drawcount, maxdrawcount, stride,
                                       4 * sizeof(Uint32), __func__)) {
            return;
        }
        // See MultiDrawElementsIndirectCount, including why this one goes last.
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount;
        if (!multiDrawArraysIndirectCount) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Backend does not support indirect-parameter array draws."));
            return;
        }
        MultiDrawArraysIndirectCount_Backend(mode, indirect, drawcount, maxdrawcount, stride);
    }

    void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
                                     const void* indices, GLint basevertex) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        if (!ValidateDrawElementsIndexType(__func__, type)) return;
        if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
        if (!ValidateDrawElementsRange(__func__, start, end)) return;
        DrawRangeElementsBaseVertex_Backend(mode, start, end, count, type, indices, basevertex);
    }

    void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        DrawRangeElements_Backend(mode, start, end, count, type, indices);
    }

    void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
                                                     GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        DrawElementsInstancedBaseVertexBaseInstance_Backend(mode, count, type, indices, instancecount, basevertex,
                                                            baseinstance);
    }

    void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
                                         GLsizei instancecount, GLint basevertex) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        if (!ValidateDrawElementsIndexType(__func__, type)) return;
        if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
        if (!ValidateNonNegativeDrawArgument(__func__, "instancecount", instancecount)) return;
        DrawElementsInstancedBaseVertex_Backend(mode, count, type, indices, instancecount, basevertex);
    }

    void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
                                           GLsizei instancecount, GLuint baseinstance) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        DrawElementsInstancedBaseInstance_Backend(mode, count, type, indices, instancecount, baseinstance);
    }

    void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        DrawElementsInstanced_Backend(mode, count, type, indices, instancecount);
    }

    void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        if (!ValidateDrawElementsIndexType(__func__, type)) return;
        if (!ValidateIndirectDrawSource(__func__, indirect, kDrawElementsIndirectCommandBytes)) return;
        DrawElementsIndirect_Backend(mode, type, indirect);
    }

    void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
                                         GLuint baseinstance) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        DrawArraysInstancedBaseInstance_Backend(mode, first, count, instancecount, baseinstance);
    }

    void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        DrawArraysInstanced_Backend(mode, first, count, instancecount);
    }

    void DrawArraysIndirect(GLenum mode, const void* indirect) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        if (!ValidateIndirectDrawSource(__func__, indirect, kDrawArraysIndirectCommandBytes)) return;
        DrawArraysIndirect_Backend(mode, indirect);
    }

    void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        if (!ValidateDrawElementsIndexType(__func__, type)) return;
        if (!ValidateNonNegativeDrawArgument(__func__, "count", count)) return;
        AccountTransformFeedbackPrimitives(mode, count);
        DrawElementsBaseVertex_Backend(mode, count, type, indices, basevertex);
    }

    void DrawArrays(GLenum mode, GLint first, GLsizei count) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        AccountTransformFeedbackPrimitives(mode, count);
        DrawArrays_Backend(mode, first, count);
    }

    void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        if (drawcount < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "drawcount must be non-negative."));
            return;
        }
        MultiDrawArrays_Backend(mode, first, count, drawcount);
    }

    void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
                           GLsizei drawcount) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        MultiDrawElements_Backend(mode, count, type, indices, drawcount);
    }

    void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const void* const* indices,
                                     GLsizei drawcount, const GLint* basevertex) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        if (!ValidateDrawElementsIndexType(__func__, type)) return;
        if (!ValidateNonNegativeDrawArgument(__func__, "drawcount", drawcount)) return;
        // GL 4.6 core 10.5 defines MultiDrawElementsBaseVertex as drawcount separate
        // DrawElementsBaseVertex calls, so each element of the count array carries the same
        // non-negative requirement the single-draw entry point applies to its own count. The
        // whole call is rejected before any sub-draw is issued, which is what makes the error
        // observable at all - a driver that drew the valid prefix first would leave the
        // framebuffer half-written.
        if (count != nullptr) {
            for (GLsizei draw = 0; draw < drawcount; ++draw) {
                if (!ValidateNonNegativeDrawArgument(__func__, "every element of count", count[draw])) return;
            }
        }
        MultiDrawElementsBaseVertex_Backend(mode, count, type, indices, drawcount, basevertex);
    }

    void Clear(GLbitfield mask) {
        Clear_Backend(mask);
    }

    void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
        if (!ValidatePrimitiveModeEnum(__func__, mode)) return;
        if (!PrepareCurrentProgramForDraw(__func__)) return;
        if (!ValidatePrimitiveModeForBackend(__func__, mode)) return;
        AccountTransformFeedbackPrimitives(mode, count);
        DrawElements_Backend(mode, count, type, indices);
    }

    void BeginTransformFeedback(GLenum primitiveMode) {
        if (primitiveMode != GL_POINTS && primitiveMode != GL_LINES && primitiveMode != GL_TRIANGLES) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "primitiveMode must be GL_POINTS, GL_LINES or GL_TRIANGLES."));
            return;
        }
        if (MG_State::pGLContext->IsTransformFeedbackActive()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is already active."));
            return;
        }
        const auto& program = MG_State::pGLContext->GetProgramForDraw();
        if (!program || !program->GetLinkStatus() || program->GetTransformFeedbackVaryingCount() == 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>(
                    "MG_Impl/GLImpl", __func__,
                    "No program with transform feedback varyings is active."));
            return;
        }
        // Every capture buffer slot the program's mode uses must have a buffer bound. A slot
        // of stride 0 - two consecutive gl_NextBuffer entries - captures nothing and so needs
        // no binding.
        const SizeT usedBufferCount = program->GetTransformFeedbackBufferCount();
        for (SizeT i = 0; i < usedBufferCount; ++i) {
            if (program->GetTransformFeedbackStride(static_cast<Uint32>(i)) == 0) continue;
            const auto& point = MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
                                                                            static_cast<Uint>(i));
            if (point.GetBoundObject() == nullptr) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>(
                        "MG_Impl/GLImpl", __func__,
                        "Transform feedback buffer binding point " + std::to_string(i) + " has no buffer bound."));
                return;
            }
        }
        MG_State::pGLContext->BeginTransformFeedback(primitiveMode, program);
        if (const auto beginXfb = MG_Backend::gBackendFunctionsTable.GL.BeginTransformFeedback) {
            beginXfb(primitiveMode);
        }
    }

    // Vulkan transform feedback captures triangle strips in plain (i, i+1, i+2)
    // vertex order, but GL decomposes odd strip triangles as (i+1, i, i+2)
    // (GL 4.6 table 10.1). With the geometry stage's statically-known strip
    // lengths the captured records are reordered in place: swap the first two
    // vertex records of every odd triangle within each emitted strip.
    static void FixupGsStripCaptureOrder(const SharedPtr<MG_State::GLState::ProgramObject>& program,
                                         Uint64 inputPrimitives) {
        // Only Vulkan-order captures need this. A backend that runs the capture on its
        // own GL/ES driver (it owns the span, hence the EndTransformFeedback entry) has
        // already produced GL's vertex order, and reordering it again would corrupt it.
        if (MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback != nullptr) {
            return;
        }
        if (program == nullptr || !program->HasGsTriangleStripCaptureFixup() || inputPrimitives == 0) {
            return;
        }
        const auto& stripTriangles = program->GetGsStripTriangles();

        // Global triangle indices whose leading vertex pair must swap.
        Vector<Uint64> swapTriangles;
        Uint64 triangleBase = 0;
        for (Uint64 input = 0; input < inputPrimitives; ++input) {
            for (const Uint32 stripLength : stripTriangles) {
                for (Uint32 t = 1; t < stripLength; t += 2) {
                    swapTriangles.push_back(triangleBase + t);
                }
                triangleBase += stripLength;
            }
        }
        if (swapTriangles.empty()) {
            return;
        }

        for (SizeT bufferIndex = 0; bufferIndex < program->GetTransformFeedbackBufferCount(); ++bufferIndex) {
            const Uint32 stride = program->GetTransformFeedbackStride(static_cast<Uint32>(bufferIndex));
            if (stride == 0) continue;
            const auto& bindingPoint =
                MG_State::pGLContext->GetBufferBindingPoint(BufferTarget::TransformFeedback,
                                                            static_cast<Uint>(bufferIndex));
            const auto& buffer = bindingPoint.GetBoundObject();
            if (buffer == nullptr) continue;
            const Range1D range = bindingPoint.GetRange();
            const Uint8* mapped = buffer->MappedData();
            if (mapped == nullptr) continue;
            // The geometry stage amplifies, so the CPU vertex counter does not bound
            // the capture; the binding range's whole-triangle capacity does.
            const Uint64 rangeBytes = range.end > range.start ? static_cast<Uint64>(range.end - range.start) : 0;
            const Uint64 capturedTriangles = std::min<Uint64>(triangleBase, (rangeBytes / stride) / 3);

            // Observed Vulkan capture order for odd strip triangles is (i, i+2, i+1)
            // (winding preserved by swapping the trailing pair); GL wants
            // (i+1, i, i+2), which is one rotation away: (a,b,c) -> (c,a,b).
            Vector<Uint8> scratch(stride);
            for (const Uint64 triangle : swapTriangles) {
                if (triangle >= capturedTriangles) break;
                const SizeT v0Offset = static_cast<SizeT>(range.start) + static_cast<SizeT>(triangle * 3) * stride;
                const SizeT v1Offset = v0Offset + stride;
                const SizeT v2Offset = v1Offset + stride;
                Memcpy(scratch.data(), mapped + v2Offset, stride);
                buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v1Offset, stride}, v2Offset);
                buffer->WritebackFromBackend({const_cast<Uint8*>(mapped) + v0Offset, stride}, v1Offset);
                buffer->WritebackFromBackend({scratch.data(), stride}, v0Offset);
            }
        }
    }

    void EndTransformFeedback(void) {
        if (!MG_State::pGLContext->IsTransformFeedbackActive()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not active."));
            return;
        }
        const auto capturedProgram = MG_State::pGLContext->GetTransformFeedbackProgram();
        const Uint64 inputPrimitives = MG_State::pGLContext->GetTransformFeedbackInputPrimitives();
        // Closed while the capture state is still active: a backend that captures
        // through its own driver reads the capture program and buffer bindings here.
        if (const auto endXfb = MG_Backend::gBackendFunctionsTable.GL.EndTransformFeedback) {
            endXfb();
        }
        MG_State::pGLContext->EndTransformFeedback();
        // Captured results must be visible to MapBuffer/GetBufferSubData after
        // End; the capture targets are host-coherent GPU memory, so completing
        // the GPU work is all that is required.
        auto& backendGL = MG_Backend::gBackendFunctionsTable.GL;
        if (backendGL.FenceSync && backendGL.ClientWaitSync) {
            if (auto sync = backendGL.FenceSync()) {
                backendGL.ClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, ~0ull);
                if (backendGL.DeleteSync) {
                    backendGL.DeleteSync(sync);
                }
            }
        }
        FixupGsStripCaptureOrder(capturedProgram, inputPrimitives);
    }

    void PauseTransformFeedback(void) {
        if (!MG_State::pGLContext->IsTransformFeedbackActive() ||
            MG_State::pGLContext->IsTransformFeedbackPaused()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Transform feedback is not active, or is already paused."));
            return;
        }
        MG_State::pGLContext->SetTransformFeedbackPaused(true);
        if (const auto pauseXfb = MG_Backend::gBackendFunctionsTable.GL.PauseTransformFeedback) {
            pauseXfb();
        }
    }

    void ResumeTransformFeedback(void) {
        if (!MG_State::pGLContext->IsTransformFeedbackActive() ||
            !MG_State::pGLContext->IsTransformFeedbackPaused()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Transform feedback is not paused."));
            return;
        }
        MG_State::pGLContext->SetTransformFeedbackPaused(false);
        if (const auto resumeXfb = MG_Backend::gBackendFunctionsTable.GL.ResumeTransformFeedback) {
            resumeXfb();
        }
    }

    void GenTransformFeedbacks(GLsizei n, GLuint* ids) {
        if (n < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
            return;
        }
        if (n == 0 || ids == nullptr) return;
        Vector<Uint> names;
        MG_State::pGLContext->GenTransformFeedbackNames(static_cast<Uint>(n), names);
        Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
    }

    void CreateTransformFeedbacks(GLsizei n, GLuint* ids) {
        if (n < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
            return;
        }
        if (n == 0 || ids == nullptr) return;
        Vector<Uint> names;
        MG_State::pGLContext->GenTransformFeedbackNames(static_cast<Uint>(n), names);
        // Unlike glGenTransformFeedbacks, the names are objects immediately: there is no bind step
        // to create them from (GL 4.6 core 13.2.1).
        for (const Uint name : names) {
            MG_State::pGLContext->CreateTransformFeedbackObject(name);
        }
        Memcpy(ids, names.data(), static_cast<SizeT>(n) * sizeof(GLuint));
    }

    namespace {
        // Shared front half of the by-name transform feedback entry points: the object has to exist
        // (INVALID_OPERATION otherwise) before anything else about the call is looked at.
        Bool ValidateNamedTransformFeedback(GLuint xfb, const char* functionName) {
            if (!MG_State::pGLContext->IsTransformFeedbackObject(xfb)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                                 std::to_string(xfb) + " is not a transform feedback object."));
                return false;
            }
            return true;
        }

        Bool ValidateTransformFeedbackBufferIndex(GLuint index, const char* functionName) {
            if (index >= MG_State::GLState::GLContext::MAX_TRANSFORM_FEEDBACK_BUFFERS) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidValue,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                                 "index exceeds GL_MAX_TRANSFORM_FEEDBACK_BUFFERS."));
                return false;
            }
            return true;
        }

        // A capture binding may not be changed while the object is capturing (GL 4.6 core 13.2.2).
        Bool ValidateNamedTransformFeedbackNotActive(GLuint xfb, const char* functionName) {
            if (MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                                 "The transform feedback object is capturing."));
                return false;
            }
            return true;
        }

        SharedPtr<MG_State::GLState::BufferObject> ResolveTransformFeedbackBuffer(GLuint buffer,
                                                                                 const char* functionName) {
            if (buffer == 0) return nullptr;
            if (!MG_State::pGLContext->ValidateBufferName(buffer)) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                                 std::to_string(buffer) + " is not a buffer object."));
                return nullptr;
            }
            return MG_State::pGLContext->GetBufferObject(buffer);
        }
    } // namespace

    void TransformFeedbackBufferBase(GLuint xfb, GLuint index, GLuint buffer) {
        if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
        if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
        if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return;
        if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             std::to_string(buffer) + " is not a buffer object."));
            return;
        }
        MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index,
                                                               ResolveTransformFeedbackBuffer(buffer, __func__), {},
                                                               false);
    }

    void TransformFeedbackBufferRange(GLuint xfb, GLuint index, GLuint buffer, GLintptr offset, GLsizeiptr size) {
        if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
        if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
        if (!ValidateNamedTransformFeedbackNotActive(xfb, __func__)) return;
        if (offset < 0 || size <= 0 || (offset % 4) != 0 || (size % 4) != 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "offset and size must be non-negative multiples of 4."));
            return;
        }
        if (buffer != 0 && !MG_State::pGLContext->ValidateBufferName(buffer)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             std::to_string(buffer) + " is not a buffer object."));
            return;
        }
        auto bufferObject = ResolveTransformFeedbackBuffer(buffer, __func__);
        const Range1D range{static_cast<SizeT>(offset), static_cast<SizeT>(offset) + static_cast<SizeT>(size)};
        MG_State::pGLContext->SetNamedTransformFeedbackBinding(xfb, index, bufferObject, range,
                                                               bufferObject != nullptr);
    }

    void GetTransformFeedbackiv(GLuint xfb, GLenum pname, GLint* param) {
        if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
        if (!param) return;
        switch (pname) {
        case GL_TRANSFORM_FEEDBACK_ACTIVE:
            *param = MG_State::pGLContext->IsNamedTransformFeedbackActive(xfb) ? GL_TRUE : GL_FALSE;
            return;
        case GL_TRANSFORM_FEEDBACK_PAUSED:
            *param = MG_State::pGLContext->IsNamedTransformFeedbackPaused(xfb) ? GL_TRUE : GL_FALSE;
            return;
        default:
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "pname must be GL_TRANSFORM_FEEDBACK_ACTIVE or _PAUSED."));
            return;
        }
    }

    void GetTransformFeedbacki_v(GLuint xfb, GLenum pname, GLuint index, GLint* param) {
        if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
        if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_BINDING) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "pname must be GL_TRANSFORM_FEEDBACK_BUFFER_BINDING."));
            return;
        }
        if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
        if (!param) return;
        const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index);
        *param = binding.Buffer ? static_cast<GLint>(binding.Buffer->GetExternalIndex()) : 0;
    }

    void GetTransformFeedbacki64_v(GLuint xfb, GLenum pname, GLuint index, GLint64* param) {
        if (!ValidateNamedTransformFeedback(xfb, __func__)) return;
        if (pname != GL_TRANSFORM_FEEDBACK_BUFFER_START && pname != GL_TRANSFORM_FEEDBACK_BUFFER_SIZE) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "pname must be GL_TRANSFORM_FEEDBACK_BUFFER_START or _SIZE."));
            return;
        }
        if (!ValidateTransformFeedbackBufferIndex(index, __func__)) return;
        if (!param) return;
        const auto binding = MG_State::pGLContext->GetNamedTransformFeedbackBinding(xfb, index);
        // glTransformFeedbackBufferBase leaves both at zero; only the range form sets them
        // (GL 4.6 core table 23.48).
        if (!binding.Buffer || !binding.HasExplicitRange) {
            *param = 0;
            return;
        }
        *param = (pname == GL_TRANSFORM_FEEDBACK_BUFFER_START)
                     ? static_cast<GLint64>(binding.Range.start)
                     : static_cast<GLint64>(binding.Range.end - binding.Range.start);
    }

    void DeleteTransformFeedbacks(GLsizei n, const GLuint* ids) {
        if (n < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "n must be non-negative."));
            return;
        }
        if (ids == nullptr) return;
        for (GLsizei i = 0; i < n; ++i) {
            const GLuint id = ids[i];
            // Unknown names and 0 are silently ignored; an object whose capture span is
            // still open is not (GL 4.6 core 13.2.1).
            if (id == 0 || !MG_State::pGLContext->ValidateTransformFeedbackName(id)) continue;
            if (id == MG_State::pGLContext->GetBoundTransformFeedbackName() &&
                MG_State::pGLContext->IsTransformFeedbackActive()) {
                MG_State::pGLContext->RecordError(
                    ErrorCode::InvalidOperation,
                    MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                                 "Cannot delete a transform feedback object whose capture is active."));
                continue;
            }
            if (const auto deleteXfb = MG_Backend::gBackendFunctionsTable.GL.DeleteTransformFeedback) {
                deleteXfb(id);
            }
            MG_State::pGLContext->MarkTransformFeedbackObjectForDeletion(id);
        }
    }

    void BindTransformFeedback(GLenum target, GLuint id) {
        if (target != GL_TRANSFORM_FEEDBACK) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidEnum,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "target must be GL_TRANSFORM_FEEDBACK."));
            return;
        }
        // A running capture pins its object; only a paused one may be swapped out.
        if (MG_State::pGLContext->IsTransformFeedbackActive() &&
            !MG_State::pGLContext->IsTransformFeedbackPaused()) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             "Transform feedback is active and not paused."));
            return;
        }
        if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
                                             std::to_string(id) + " is not a transform feedback object name."));
            return;
        }
        MG_State::pGLContext->BindTransformFeedbackObject(id);
        if (const auto bindXfb = MG_Backend::gBackendFunctionsTable.GL.BindTransformFeedback) {
            bindXfb(id);
        }
    }

    GLboolean IsTransformFeedback(GLuint id) {
        // Name 0 is the default object, and a name glGenTransformFeedbacks handed out only
        // becomes the name of an object once it has been bound.
        return MG_State::pGLContext->IsTransformFeedbackObject(id) ? GL_TRUE : GL_FALSE;
    }

    // glDrawTransformFeedback[Stream][Instanced]: replays the vertices the named object
    // captured in its last completed span, as if by glDrawArraysInstanced with that count
    // (GL 4.6 core 10.3.7).
    static void DrawTransformFeedbackImpl(const char* functionName, GLenum mode, GLuint id, GLuint stream,
                                          GLsizei instancecount) {
        if (!PrepareCurrentProgramForDraw(functionName)) return;
        if (!ValidatePrimitiveModeForBackend(functionName, mode)) return;
        if (instancecount < 0) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName, "instancecount must be non-negative."));
            return;
        }
        // "id is not the name of a transform feedback object" has to mean the same thing here
        // as it does to glIsTransformFeedback, and the two predicates are not interchangeable:
        // a name glGenTransformFeedbacks handed out is only reserved until it is first bound,
        // and only the bind turns it into an object (GL 4.6 core 13.2.1). ValidateTransformFeedbackName
        // answers the reservation question - the right one for glBindTransformFeedback, which is
        // what turns a reserved name into an object - so using it here let a generated-but-unbound
        // name through to the completed-span check below and raised INVALID_OPERATION where the
        // spec asks for INVALID_VALUE. Name 0 is the default object and always drawable.
        if (id != 0 && !MG_State::pGLContext->IsTransformFeedbackObject(id)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             std::to_string(id) + " is not a transform feedback object name."));
            return;
        }
        // GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which this implementation
        // answers as 1 - so stream 0 is the only one that exists and anything else is
        // INVALID_VALUE. Read from the getter rather than written as `stream != 0` so the two can
        // never drift: if vertex-stream support ever lands, this bound moves with the limit.
        GLint maxVertexStreams = 1;
        GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams);
        if (stream >= static_cast<GLuint>(std::max(maxVertexStreams, 1))) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidValue,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             "stream must be less than GL_MAX_VERTEX_STREAMS."));
            return;
        }
        // Drawing from an object whose capture is currently open is legal and deliberate:
        // it is how a transform feedback result is fed straight back into the next span
        // (ARB_transform_feedback2 lists no such restriction).
        if (!MG_State::pGLContext->HasTransformFeedbackCompletedSpan(id)) {
            MG_State::pGLContext->RecordError(
                ErrorCode::InvalidOperation,
                MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", functionName,
                                             "glEndTransformFeedback has never been called for this object."));
            return;
        }

        // `stream` is provably 0 here (the bound above is 1), so this is stream 0's record.
        const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id);
        if (vertices == 0) return;
        const auto count = static_cast<GLsizei>(vertices);
        AccountTransformFeedbackPrimitives(mode, count);
        if (instancecount == 1) {
            DrawArrays_Backend(mode, 0, count);
        } else {
            DrawArraysInstanced_Backend(mode, 0, count, instancecount);
        }
    }

    void DrawTransformFeedback(GLenum mode, GLuint id) {
        DrawTransformFeedbackImpl(__func__, mode, id, 0, 1);
    }

    void DrawTransformFeedbackInstanced(GLenum mode, GLuint id, GLsizei instancecount) {
        DrawTransformFeedbackImpl(__func__, mode, id, 0, instancecount);
    }

    void DrawTransformFeedbackStream(GLenum mode, GLuint id, GLuint stream) {
        DrawTransformFeedbackImpl(__func__, mode, id, stream, 1);
    }

    void DrawTransformFeedbackStreamInstanced(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount) {
        DrawTransformFeedbackImpl(__func__, mode, id, stream, instancecount);
    }

} // namespace MobileGL::MG_Impl::GLImpl
