// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/VertexAttribBindingScenario.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
//
// ARB_vertex_attrib_binding: the separate format/binding state the GL 4.3 vertex
// input model is made of, read back out of the draw that consumed it.
//
// Every scenario here captures the vertex shader's inputs with transform feedback
// under GL_RASTERIZER_DISCARD, which is what the KHR-GL43.vertex_attrib_binding
// cases do: the captured record IS the fetched vertex, so "the binding state did
// not reach the draw" and "the draw fetched the wrong bytes" are distinguishable
// from each other and from "the capture did not run" (the buffer is pre-filled
// with a poison value).

#include <cstdio>
#include <cstring>
#include <string>
#include <vector>

#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"

#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES

namespace MGITest {
    namespace {

        constexpr float kPoison = -1234.0f;

        GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
            const GLuint shader = glCreateShader(type);
            const char* text = source.c_str();
            glShaderSource(shader, 1, &text, nullptr);
            glCompileShader(shader);
            GLint status = GL_FALSE;
            glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
            if (status == GL_FALSE) {
                GLint length = 0;
                glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
                std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
                glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
                if (log != nullptr) *log = buffer.data();
                glDeleteShader(shader);
                return 0;
            }
            return shader;
        }

        // A vertex-only capture program, exactly how the CTS builds one: the varying
        // names are declared before the link and the fragment stage is absent because
        // the draw runs under GL_RASTERIZER_DISCARD.
        GLuint BuildCaptureProgram(const std::string& vertexSource, const std::vector<const char*>& xfbVaryings,
                                   std::string* log) {
            const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
            if (vertexShader == 0) return 0;
            const GLuint program = glCreateProgram();
            glAttachShader(program, vertexShader);
            if (!xfbVaryings.empty()) {
                glTransformFeedbackVaryings(program, static_cast<GLsizei>(xfbVaryings.size()), xfbVaryings.data(),
                                            GL_INTERLEAVED_ATTRIBS);
            }
            glLinkProgram(program);
            glDeleteShader(vertexShader);
            GLint status = GL_FALSE;
            glGetProgramiv(program, GL_LINK_STATUS, &status);
            if (status == GL_FALSE) {
                GLint length = 0;
                glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
                std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
                glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
                if (log != nullptr) *log = buffer.data();
                glDeleteProgram(program);
                return 0;
            }
            return program;
        }

        // Four float inputs at locations 0..3, captured as four vec4s per vertex.
        // Locations the test does not feed keep their current-attribute value, which
        // every scenario sets to a known constant first.
        std::string CaptureVertexSource() {
            return R"(#version 430 core
layout(location = 0) in vec4 vs_in_attrib0;
layout(location = 1) in vec4 vs_in_attrib1;
layout(location = 2) in vec4 vs_in_attrib2;
layout(location = 3) in vec4 vs_in_attrib3;
out StageData {
  vec4 attrib0;
  vec4 attrib1;
  vec4 attrib2;
  vec4 attrib3;
} vs_out;
void main() {
  vs_out.attrib0 = vs_in_attrib0;
  vs_out.attrib1 = vs_in_attrib1;
  vs_out.attrib2 = vs_in_attrib2;
  vs_out.attrib3 = vs_in_attrib3;
}
)";
        }

        std::vector<const char*> CaptureVaryingNames() {
            return {"StageData.attrib0", "StageData.attrib1", "StageData.attrib2", "StageData.attrib3"};
        }

        // Runs `vertexCount` x `instanceCount` points through the capture program and
        // returns the interleaved floats (16 per point: four vec4s).
        std::vector<float> CapturePoints(GLuint program, GLuint xfbBuffer, int vertexCount, int instanceCount) {
            const std::size_t floats = static_cast<std::size_t>(vertexCount) * instanceCount * 16;
            std::vector<float> poison(floats, kPoison);
            glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
            glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(floats * sizeof(float)), poison.data(),
                         GL_DYNAMIC_DRAW);

            glEnable(GL_RASTERIZER_DISCARD);
            glUseProgram(program);
            glBeginTransformFeedback(GL_POINTS);
            glDrawArraysInstanced(GL_POINTS, 0, vertexCount, instanceCount);
            glEndTransformFeedback();
            glDisable(GL_RASTERIZER_DISCARD);

            std::vector<float> data(floats, kPoison);
            glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(floats * sizeof(float)),
                               data.data());
            glUseProgram(0);
            return data;
        }

        // As CapturePoints, but through the baseInstance entry point, and on a capture buffer
        // of its own.
        //
        // Kept separate from CapturePoints rather than defaulting a parameter, so that every
        // existing caller stays on the draw command that carries no baseInstance at all: the
        // negative control is then a DIFFERENT command rather than the same one passed a zero.
        //
        // The buffer per capture is a leftover. baseInstance was the first thing here that
        // needed several captures in ONE test, and at the time a second capture into the same
        // buffer object came back empty on DirectVulkan - respecifying a buffer whose bytes the
        // backend had handed the frontend a pointer into replaced the storage under that
        // pointer, so the capture wrote one store and the readback read another. That is fixed
        // and pinned by XfbCaptureBufferReuseScenario, which owns the shape now; a buffer per
        // capture is simply the cheapest thing that still isolates these three draws from each
        // other.
        std::vector<float> CaptureOwnBufferBaseInstance(GLuint program, int vertexCount, int instanceCount,
                                                        GLuint baseInstance, bool useBaseInstanceCommand) {
            const std::size_t floats = static_cast<std::size_t>(vertexCount) * instanceCount * 16;
            std::vector<float> poison(floats, kPoison);
            GLuint xfbBuffer = 0;
            glGenBuffers(1, &xfbBuffer);
            glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
            glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(floats * sizeof(float)), poison.data(),
                         GL_DYNAMIC_DRAW);

            glEnable(GL_RASTERIZER_DISCARD);
            glUseProgram(program);
            glBeginTransformFeedback(GL_POINTS);
            if (useBaseInstanceCommand) {
                glDrawArraysInstancedBaseInstance(GL_POINTS, 0, vertexCount, instanceCount, baseInstance);
            } else {
                glDrawArraysInstanced(GL_POINTS, 0, vertexCount, instanceCount);
            }
            glEndTransformFeedback();
            glDisable(GL_RASTERIZER_DISCARD);

            std::vector<float> data(floats, kPoison);
            glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(floats * sizeof(float)),
                               data.data());
            glUseProgram(0);
            glDeleteBuffers(1, &xfbBuffer);
            return data;
        }

        // point p, attribute a, component c
        float At(const std::vector<float>& data, int point, int attrib, int component) {
            const std::size_t index = static_cast<std::size_t>(point) * 16 + attrib * 4 + component;
            return index < data.size() ? data[index] : kPoison;
        }

        void ResetCurrentAttribs() {
            for (GLuint i = 0; i < 4; ++i) {
                glVertexAttrib4f(i, 0.0f, 0.0f, 0.0f, 0.0f);
            }
        }

        ::testing::AssertionResult Vec4Is(const std::vector<float>& data, int point, int attrib, float x, float y,
                                          float z, float w) {
            const float gx = At(data, point, attrib, 0);
            const float gy = At(data, point, attrib, 1);
            const float gz = At(data, point, attrib, 2);
            const float gw = At(data, point, attrib, 3);
            const float tolerance = 0.01f;
            auto close = [tolerance](float a, float b) { return (a - b) < tolerance && (b - a) < tolerance; };
            if (close(gx, x) && close(gy, y) && close(gz, z) && close(gw, w)) {
                return ::testing::AssertionSuccess();
            }
            return ::testing::AssertionFailure()
                   << "point " << point << " attribute " << attrib << " is (" << gx << ", " << gy << ", " << gz << ", "
                   << gw << "), expected (" << x << ", " << y << ", " << z << ", " << w << ")";
        }

    } // namespace

    class VertexAttribBindingScenario : public ScenarioTest {
    protected:
        void SetUp() override {
            ScenarioTest::SetUp();
            if (!Ready()) return;
            m_program = BuildCaptureProgram(CaptureVertexSource(), CaptureVaryingNames(), &m_log);
            ASSERT_NE(m_program, 0u) << "capture program did not link: " << m_log;
            glGenVertexArrays(1, &m_vao);
            glGenBuffers(1, &m_xfbo);
            glBindVertexArray(m_vao);
        }

        void TearDown() override {
            if (!Ready()) return;
            glBindVertexArray(0);
            glDeleteVertexArrays(1, &m_vao);
            glDeleteBuffers(1, &m_xfbo);
            glDeleteProgram(m_program);
        }

        GLuint m_program = 0;
        GLuint m_vao = 0;
        GLuint m_xfbo = 0;
        std::string m_log;
    };

    // glVertexAttribFormat + glBindVertexBuffer + glVertexAttribBinding, in the order
    // the CTS uses (buffer first, then format, then binding), must feed the draw.
    TEST_F(VertexAttribBindingScenario, FormatAndBindingFeedTheDraw) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glBindVertexBuffer(0, vbo, 0, 12);
        glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 0);
        glVertexAttribBinding(1, 0);
        glEnableVertexAttribArray(1);

        const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
        EXPECT_TRUE(Vec4Is(data, 0, 1, 1.0f, 2.0f, 3.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 1, 4.0f, 5.0f, 6.0f, 1.0f));
        // An attribute nothing configured still reports its current value.
        EXPECT_TRUE(Vec4Is(data, 0, 0, 0.0f, 0.0f, 0.0f, 0.0f));

        glDisableVertexAttribArray(1);
        glDeleteBuffers(1, &vbo);
    }

    // The reverse order - format and binding declared before any buffer exists on the
    // binding point - has to resolve to the same thing once glBindVertexBuffer lands.
    TEST_F(VertexAttribBindingScenario, FormatBeforeBufferStillResolves) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glVertexAttribBinding(2, 3);
        glVertexAttribFormat(2, 2, GL_FLOAT, GL_FALSE, 4);
        glEnableVertexAttribArray(2);
        glBindVertexBuffer(3, vbo, 0, 12);

        const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
        EXPECT_TRUE(Vec4Is(data, 0, 2, 2.0f, 3.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 2, 5.0f, 6.0f, 0.0f, 1.0f));

        glDisableVertexAttribArray(2);
        glDeleteBuffers(1, &vbo);
    }

    // GL 4.6 core 10.3.1: a binding point's stride is the byte distance between
    // consecutive elements, and zero means every vertex reads the SAME element. That
    // is the opposite of glVertexAttribPointer's stride 0, which means "tightly
    // packed" - the two spellings must not be collapsed into one another.
    TEST_F(VertexAttribBindingScenario, BindingStrideZeroRepeatsOneElement) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glVertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, 0);
        glVertexAttribBinding(0, 5);
        glBindVertexBuffer(5, vbo, 16, 0);
        glEnableVertexAttribArray(0);

        const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
        EXPECT_TRUE(Vec4Is(data, 0, 0, 5.0f, 6.0f, 7.0f, 8.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 0, 5.0f, 6.0f, 7.0f, 8.0f));

        glDisableVertexAttribArray(0);
        glDeleteBuffers(1, &vbo);
    }

    // The pointer API keeps its own meaning of stride 0 (tightly packed) even though
    // it is defined in terms of the binding model - the negative control for the
    // scenario above.
    TEST_F(VertexAttribBindingScenario, PointerStrideZeroStaysTightlyPacked) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
        glEnableVertexAttribArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
        EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 2.0f, 3.0f, 4.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 0, 5.0f, 6.0f, 7.0f, 8.0f));

        glDisableVertexAttribArray(0);
        glDeleteBuffers(1, &vbo);
    }

    // glVertexBindingDivisor is per BINDING POINT: it has to reach every attribute
    // pointed at that binding, and the instance step must honour the divisor rather
    // than advancing once per instance.
    TEST_F(VertexAttribBindingScenario, BindingDivisorAppliesToEveryAttributeOnThePoint) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const float vertices[] = {10.0f, 20.0f, 30.0f, 40.0f};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0);
        glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 4);
        glVertexAttribBinding(0, 4);
        glVertexAttribBinding(1, 4);
        glBindVertexBuffer(4, vbo, 0, 8);
        glVertexBindingDivisor(4, 2);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);

        // The divisor is per binding point, so it has to be visible on BOTH attributes
        // pointed at it - and this query is what separates "the frontend never resolved
        // it" from "the backend did not apply it".
        GLint divisor = -1;
        glGetVertexAttribiv(0, GL_VERTEX_ATTRIB_ARRAY_DIVISOR, &divisor);
        EXPECT_EQ(divisor, 2);
        divisor = -1;
        glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_DIVISOR, &divisor);
        EXPECT_EQ(divisor, 2);

        // 1 vertex x 4 instances, divisor 2: instances 0,1 read element 0 and
        // instances 2,3 read element 1.
        const std::vector<float> data = CapturePoints(m_program, m_xfbo, 1, 4);
        EXPECT_TRUE(Vec4Is(data, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 0, 10.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 2, 0, 30.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 3, 0, 30.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 0, 1, 20.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 2, 1, 40.0f, 0.0f, 0.0f, 1.0f));

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
        glDeleteBuffers(1, &vbo);
    }

    // baseInstance moves the ELEMENT the instanced arrays start at. DirectGLES has no
    // ES entry point that says so on the drivers we ship against (GL_EXT_base_instance
    // is absent on Adreno), so it folds the shift into the attribute's own offset - and
    // the thing that made this worth pinning is that the value used to reach the shader
    // uniform for gl_BaseInstance and NEVER the fetch, so a draw could report a base
    // instance it had not actually read from.
    //
    // The three draws are the point. Zero first as a negative control, so a backend that
    // simply ignored baseInstance could not pass on the middle draw alone; and zero AGAIN
    // last, because the shift is emitted into per-attribute state the VAO twin memoises -
    // leaving it applied would make every subsequent ordinary draw fetch from the wrong
    // element, which is a far worse bug than the one being fixed.
    TEST_F(VertexAttribBindingScenario, BaseInstanceMovesTheInstancedArraysStartElement) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const float instanceData[] = {10.0f, 20.0f, 30.0f, 40.0f};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(instanceData), instanceData, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0);
        glVertexAttribBinding(0, 0);
        glBindVertexBuffer(0, vbo, 0, 4);
        glVertexBindingDivisor(0, 1);
        glEnableVertexAttribArray(0);

        const std::vector<float> atZero = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, true);
        EXPECT_TRUE(Vec4Is(atZero, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(atZero, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f));

        const std::vector<float> atTwo = CaptureOwnBufferBaseInstance(m_program, 1, 2, 2, true);
        EXPECT_TRUE(Vec4Is(atTwo, 0, 0, 30.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(atTwo, 1, 0, 40.0f, 0.0f, 0.0f, 1.0f));

        // Nothing about the vertex array changed between these two draws, so only a
        // backend that actively un-shifts on a baseInstance change gets back to 10/20.
        const std::vector<float> backToZero = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, true);
        EXPECT_TRUE(Vec4Is(backToZero, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(backToZero, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f));

        // And a draw command with no baseInstance parameter at all must be unaffected by
        // the one that came before it.
        const std::vector<float> plain = CaptureOwnBufferBaseInstance(m_program, 1, 2, 0, false);
        EXPECT_TRUE(Vec4Is(plain, 0, 0, 10.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(plain, 1, 0, 20.0f, 0.0f, 0.0f, 1.0f));

        glDisableVertexAttribArray(0);
        glDeleteBuffers(1, &vbo);
    }

    // baseInstance is defined against the instanced arrays only: an array with divisor 0
    // advances per VERTEX and its start element is "first", which baseInstance does not
    // touch. An emulation that shifted by offset without checking the divisor would move
    // this one too, and nothing in the case above would notice.
    TEST_F(VertexAttribBindingScenario, BaseInstanceLeavesPerVertexArraysWhereTheyWere) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const float perVertex[] = {1.0f, 2.0f, 3.0f, 4.0f};
        const float perInstance[] = {10.0f, 20.0f, 30.0f, 40.0f};
        GLuint buffers[2] = {0, 0};
        glGenBuffers(2, buffers);
        glBindBuffer(GL_ARRAY_BUFFER, buffers[0]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(perVertex), perVertex, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, buffers[1]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(perInstance), perInstance, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glVertexAttribFormat(0, 1, GL_FLOAT, GL_FALSE, 0);
        glVertexAttribBinding(0, 0);
        glBindVertexBuffer(0, buffers[0], 0, 4);
        glVertexBindingDivisor(0, 0);
        glEnableVertexAttribArray(0);

        glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 0);
        glVertexAttribBinding(1, 1);
        glBindVertexBuffer(1, buffers[1], 0, 4);
        glVertexBindingDivisor(1, 1);
        glEnableVertexAttribArray(1);

        // 2 vertices x 2 instances, baseInstance 2. Points come out instance-major.
        const std::vector<float> data = CaptureOwnBufferBaseInstance(m_program, 2, 2, 2, true);
        EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 0, 2.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 2, 0, 1.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 3, 0, 2.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 0, 1, 30.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 1, 30.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 2, 1, 40.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 3, 1, 40.0f, 0.0f, 0.0f, 1.0f));

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
        glDeleteBuffers(2, buffers);
    }

    // Two attributes on one binding point at different relative offsets, plus a
    // binding offset: the fetch address is binding offset + relative offset, and the
    // relative offset must not leak into the binding's own offset.
    TEST_F(VertexAttribBindingScenario, RelativeOffsetComposesWithBindingOffset) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const float vertices[] = {0.0f, 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
        glVertexAttribFormat(1, 1, GL_FLOAT, GL_FALSE, 8);
        glVertexAttribBinding(0, 1);
        glVertexAttribBinding(1, 1);
        glBindVertexBuffer(1, vbo, 8, 12);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);

        const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
        EXPECT_TRUE(Vec4Is(data, 0, 0, 1.0f, 2.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 0, 1, 3.0f, 0.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 0, 4.0f, 5.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 1, 6.0f, 0.0f, 0.0f, 1.0f));

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
        glDeleteBuffers(1, &vbo);
    }

    // The KHR-GL43.vertex_attrib_binding.basic-input* capture program verbatim: a
    // 16-element vec4 input ARRAY at location 0, copied element by element into a
    // 16-element array inside an output interface block, all 16 members captured.
    // Every one of the 17 basic-input* cases is built on it, so a backend that cannot
    // produce this program fails all of them with "the draw captured zeros" and no
    // other symptom.
    TEST_F(VertexAttribBindingScenario, InputArrayCaptureProgramFeedsTheDraw) {
        if (!Ready()) GTEST_SKIP();

        const std::string vs = R"(#version 430 core
layout(location = 0) in vec4 vs_in_attrib[16];
out StageData {
  vec4 attrib[16];
} vs_out;
void main() {
  for (int i = 0; i < vs_in_attrib.length(); ++i) {
    vs_out.attrib[i] = vs_in_attrib[i];
  }
}
)";
        std::vector<std::string> names;
        for (int i = 0; i < 16; ++i) names.push_back("StageData.attrib[" + std::to_string(i) + "]");
        std::vector<const char*> varyings;
        for (const auto& n : names) varyings.push_back(n.c_str());

        std::string log;
        const GLuint program = BuildCaptureProgram(vs, varyings, &log);
        ASSERT_NE(program, 0u) << "capture program did not link: " << log;

        for (GLuint i = 0; i < 16; ++i) glVertexAttrib4f(i, 0.0f, 0.0f, 0.0f, 0.0f);

        const float vertices[] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glBindVertexBuffer(0, vbo, 0, 12);
        glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 0);
        glVertexAttribBinding(1, 0);
        glEnableVertexAttribArray(1);

        // 16 vec4s per point rather than the 4 the shared helper assumes.
        constexpr std::size_t kFloatsPerPoint = 64;
        std::vector<float> poison(kFloatsPerPoint * 2, kPoison);
        glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_xfbo);
        glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
                     poison.data(), GL_DYNAMIC_DRAW);
        glEnable(GL_RASTERIZER_DISCARD);
        glUseProgram(program);
        glBeginTransformFeedback(GL_POINTS);
        glDrawArrays(GL_POINTS, 0, 2);
        glEndTransformFeedback();
        glDisable(GL_RASTERIZER_DISCARD);

        std::vector<float> data(poison.size(), kPoison);
        glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
                           static_cast<GLsizeiptr>(data.size() * sizeof(float)), data.data());
        glUseProgram(0);

        // Element 0 of the array has no enabled array behind it, so it must deliver the
        // current generic attribute value set above - including its w, which is 0 here and
        // NOT the 1 an unwritten vec4 input defaults to.
        EXPECT_FLOAT_EQ(data[0], 0.0f);
        EXPECT_FLOAT_EQ(data[3], 0.0f);

        // attribute 1 of point 0 and of point 1.
        EXPECT_FLOAT_EQ(data[4], 1.0f);
        EXPECT_FLOAT_EQ(data[5], 2.0f);
        EXPECT_FLOAT_EQ(data[6], 3.0f);
        EXPECT_FLOAT_EQ(data[7], 1.0f);
        EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 4], 4.0f);
        EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 5], 5.0f);
        EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 6], 6.0f);
        EXPECT_FLOAT_EQ(data[kFloatsPerPoint + 7], 1.0f);

        glDisableVertexAttribArray(1);
        glDeleteBuffers(1, &vbo);
        glDeleteProgram(program);
    }

    // Same program, but every one of the 16 elements is asked for a DIFFERENT current value.
    //
    // An input array occupies one location per element (GL 4.6 core 11.1.1), so `in vec4 a[16]`
    // at location 0 is active on 0..15 - and the whole location span is what a backend reads to
    // decide which attributes need their current value pushed. Reflection used to record the
    // span of the ELEMENT type only, so a 16-element array claimed exactly one location: every
    // element above the first silently read the (0,0,0,1) an unwritten input defaults to instead
    // of the value glVertexAttrib4f had set. The test above could not see it, because the only
    // element it reads a current value from is element 0 - the one location the array did claim.
    TEST_F(VertexAttribBindingScenario, EveryInputArrayElementGetsItsOwnCurrentValue) {
        if (!Ready()) GTEST_SKIP();

        const std::string vs = R"(#version 430 core
layout(location = 0) in vec4 vs_in_attrib[16];
out StageData {
  vec4 attrib[16];
} vs_out;
void main() {
  for (int i = 0; i < vs_in_attrib.length(); ++i) {
    vs_out.attrib[i] = vs_in_attrib[i];
  }
}
)";
        std::vector<std::string> names;
        for (int i = 0; i < 16; ++i) names.push_back("StageData.attrib[" + std::to_string(i) + "]");
        std::vector<const char*> varyings;
        for (const auto& n : names) varyings.push_back(n.c_str());

        std::string log;
        const GLuint program = BuildCaptureProgram(vs, varyings, &log);
        ASSERT_NE(program, 0u) << "capture program did not link: " << log;

        // Distinct in every component, and never (0,0,0,1): the value an element that was
        // skipped would report has to be distinguishable from every value that was asked for.
        for (GLuint i = 0; i < 16; ++i) {
            const float base = static_cast<float>(i) + 1.0f;
            glVertexAttrib4f(i, base, base + 100.0f, base + 200.0f, base + 300.0f);
        }

        constexpr std::size_t kFloatsPerPoint = 64;
        std::vector<float> poison(kFloatsPerPoint, kPoison);
        glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_xfbo);
        glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
                     poison.data(), GL_DYNAMIC_DRAW);
        glEnable(GL_RASTERIZER_DISCARD);
        glUseProgram(program);
        glBeginTransformFeedback(GL_POINTS);
        glDrawArrays(GL_POINTS, 0, 1);
        glEndTransformFeedback();
        glDisable(GL_RASTERIZER_DISCARD);

        std::vector<float> data(poison.size(), kPoison);
        glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
                           static_cast<GLsizeiptr>(data.size() * sizeof(float)), data.data());
        glUseProgram(0);

        for (int element = 0; element < 16; ++element) {
            const float base = static_cast<float>(element) + 1.0f;
            EXPECT_FLOAT_EQ(data[element * 4 + 0], base) << "element " << element;
            EXPECT_FLOAT_EQ(data[element * 4 + 1], base + 100.0f) << "element " << element;
            EXPECT_FLOAT_EQ(data[element * 4 + 2], base + 200.0f) << "element " << element;
            EXPECT_FLOAT_EQ(data[element * 4 + 3], base + 300.0f) << "element " << element;
        }

        for (GLuint i = 0; i < 16; ++i) glVertexAttrib4f(i, 0.0f, 0.0f, 0.0f, 0.0f);
        glDeleteProgram(program);
    }

    // A GL_DOUBLE array is NARROWED to float32 and fetched, not dropped. No backend here has a
    // 64-bit vertex format, but glVertexAttribFormat(GL_DOUBLE) is defined as "doubles in memory,
    // converted to float" and the shader input is a plain vec4 either way, so nothing about fp64
    // is needed - only the fetch conversion (KHR-GL43.vertex_attrib_binding.basic-input-case4).
    // Every value here is exact in float32, so the capture is an equality test.
    TEST_F(VertexAttribBindingScenario, DoubleArrayIsFetchedAtFloat32Precision) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const double vertices[] = {100.0, 200.0, 300.0, 400.0};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glBindVertexBuffer(0, vbo, 0, 2 * static_cast<GLsizei>(sizeof(double)));
        glVertexAttribFormat(1, 2, GL_DOUBLE, GL_FALSE, 0);
        glVertexAttribBinding(1, 0);
        glEnableVertexAttribArray(1);

        const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
        EXPECT_TRUE(Vec4Is(data, 0, 1, 100.0f, 200.0f, 0.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 1, 300.0f, 400.0f, 0.0f, 1.0f));

        glDisableVertexAttribArray(1);
        glDeleteBuffers(1, &vbo);
    }

    // GL ignores `normalized` for floating-point array types, GL_DOUBLE included: the fetched
    // values are the raw ones, not scaled into [0,1]. A conversion that forwarded the flag would
    // return zeros here (KHR-GL43.vertex_attrib_binding.basic-input-case5).
    TEST_F(VertexAttribBindingScenario, NormalizedIsIgnoredForDoubleArrays) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const double vertices[] = {0.0, 10.0, 20.0, 0.0};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glBindVertexBuffer(0, vbo, 0, 4 * static_cast<GLsizei>(sizeof(double)));
        glVertexAttribFormat(2, 4, GL_DOUBLE, GL_TRUE, 0);
        glVertexAttribBinding(2, 0);
        glEnableVertexAttribArray(2);

        const std::vector<float> data = CapturePoints(m_program, m_xfbo, 1, 1);
        EXPECT_TRUE(Vec4Is(data, 0, 2, 0.0f, 10.0f, 20.0f, 0.0f));

        glDisableVertexAttribArray(2);
        glDeleteBuffers(1, &vbo);
    }

    // The LONG form asks for more precision than any backend here can give and gets the same
    // float32 stream. IsLong must not gate the narrowing off
    // (KHR-GL43.vertex_attrib_binding.advanced-bindingUpdate feeds its dvec3 this way).
    TEST_F(VertexAttribBindingScenario, LongDoubleArrayIsFetchedAtFloat32Precision) {
        if (!Ready()) GTEST_SKIP();
        ResetCurrentAttribs();

        const double vertices[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
        GLuint vbo = 0;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glBindVertexBuffer(0, vbo, 0, 3 * static_cast<GLsizei>(sizeof(double)));
        glVertexAttribLFormat(3, 3, GL_DOUBLE, 0);
        glVertexAttribBinding(3, 0);
        glEnableVertexAttribArray(3);

        const std::vector<float> data = CapturePoints(m_program, m_xfbo, 2, 1);
        EXPECT_TRUE(Vec4Is(data, 0, 3, 1.0f, 2.0f, 3.0f, 1.0f));
        EXPECT_TRUE(Vec4Is(data, 1, 3, 4.0f, 5.0f, 6.0f, 1.0f));

        glDisableVertexAttribArray(3);
        glDeleteBuffers(1, &vbo);
    }

} // namespace MGITest
