// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.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
//
// Scenario - GLSL DOUBLES, AT WHATEVER PRECISION THE BACKEND CAN GIVE.
//
// No mobile GPU has 64-bit floats. Adreno and Mali both report shaderFloat64 == VK_FALSE, so
// Magma cannot build a module that declares the Float64 capability there, and ESSL has no fp64
// type at all, so SPIRV-Cross refuses the module outright on Espryt ("FP64 not supported in ES
// profile") and the program never reaches the driver. On every such backend MobileGL narrows
// every 64-bit float in a shader to 32 bits (ShaderTranspiler::DemoteFloat64Pass) rather than
// declining the shader: `double` compiles and runs everywhere, at float precision. Where the
// backend DOES consume 64-bit floats - lavapipe is the one that does - the narrowing is skipped
// and the doubles reach the driver whole.
//
// Either way it is only half a contract. The other half is the API side: the global UBO is laid
// out by reflecting whichever module was produced, so glUniform*d has to store the width the
// shader reads, glGetUniform*v has to read that width back, and a matrix's columns are
// std140-padded to a vec4 or a dvec4 to match. Every one of those is a byte offset that fails
// silently - the uniform simply reads as something else - so the cases below set values through
// the API and have the SHADER report what it saw.
//
// WHY ALMOST EVERY EXPECTATION HERE IS A FLOAT VALUE, and why that is not an accident of the
// demotion: the shader reports through a `float` SSBO, and every value chosen is exact in
// float32, so the same number is correct in both regimes and the assertions test the LAYOUT
// rather than the precision. Exactly one case (GetUniformdvReadsBackWhatWasStored) uses a value
// that is not - 0.1 - and it names both answers explicitly.

#include <cmath>
#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 {

        // Doubles in every shape the demotion has to handle - a scalar, a vector, a matrix
        // whose column stride changes, an array whose element stride changes - all reported
        // through one float SSBO so a single readback says which one moved.
        constexpr const char* kComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
uniform double uScalar;
uniform dvec3  uVector;
uniform dmat4  uMatrix;
uniform double uArray[3];
layout(std430, binding = 0) buffer Output {
    float g_out[];
};
void main() {
    g_out[0] = float(uScalar);
    g_out[1] = float(uVector.x);
    g_out[2] = float(uVector.y);
    g_out[3] = float(uVector.z);
    // Column-major [column][row]. Off-diagonal entries catch a column-stride mistake that a
    // diagonal-only check reads straight past.
    g_out[4] = float(uMatrix[0][0]);
    g_out[5] = float(uMatrix[0][3]);
    g_out[6] = float(uMatrix[3][0]);
    g_out[7] = float(uMatrix[3][3]);
    g_out[8] = float(uArray[0]);
    g_out[9] = float(uArray[1]);
    g_out[10] = float(uArray[2]);
    // Arithmetic on doubles, including an implicit float->double conversion and a literal
    // with the fp64 suffix: this is what an application actually writes, and it is the part
    // that has to survive the conversion folding.
    double accumulated = uScalar * 2.0lf + 1.5;
    g_out[11] = float(accumulated);
}
)";

        constexpr int kOutputSlots = 12;

        class DoublePrecisionScenario : public ScenarioTest {
        protected:
            void SetUp() override {
                ScenarioTest::SetUp();
                if (!Ready()) return;
                m_program = CompileComputeProgram(kComputeSource);
                ASSERT_NE(m_program, 0u) << m_buildLog;

                glGenBuffers(1, &m_output);
                glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
                const std::vector<float> zeroes(kOutputSlots, 0.0f);
                glBufferData(GL_SHADER_STORAGE_BUFFER, kOutputSlots * sizeof(float), zeroes.data(),
                             GL_DYNAMIC_DRAW);
                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_output);
                glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
            }

            void TearDown() override {
                if (!Ready()) return;
                if (m_shapeOutput != 0) glDeleteBuffers(1, &m_shapeOutput);
                if (m_shapeProgram != 0) glDeleteProgram(m_shapeProgram);
                if (m_output != 0) glDeleteBuffers(1, &m_output);
                if (m_program != 0) glDeleteProgram(m_program);
            }

            unsigned int CompileComputeProgram(const char* source) {
                const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
                glShaderSource(shader, 1, &source, nullptr);
                glCompileShader(shader);
                GLint compiled = 0;
                glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
                if (compiled == GL_FALSE) {
                    char log[2048] = {};
                    glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
                    m_buildLog = std::string("compute shader did not compile: ") + log;
                    glDeleteShader(shader);
                    return 0;
                }
                const GLuint program = glCreateProgram();
                glAttachShader(program, shader);
                glLinkProgram(program);
                glDeleteShader(shader);
                GLint linked = 0;
                glGetProgramiv(program, GL_LINK_STATUS, &linked);
                if (linked == GL_FALSE) {
                    char log[2048] = {};
                    glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
                    m_buildLog = std::string("compute program did not link: ") + log;
                    glDeleteProgram(program);
                    return 0;
                }
                return program;
            }

            std::vector<float> Dispatch() {
                glUseProgram(m_program);
                glDispatchCompute(1, 1, 1);
                glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
                std::vector<float> values(kOutputSlots, -1.0f);
                glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
                glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kOutputSlots * sizeof(float), values.data());
                glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
                glUseProgram(0);
                return values;
            }

            unsigned int m_program = 0;
            unsigned int m_output = 0;
            unsigned int m_shapeProgram = 0;
            unsigned int m_shapeOutput = 0;
            std::string m_buildLog;
        };

        // A SHADER STORAGE BLOCK that holds doubles is the one place the narrowing is NOT free:
        // demoting `double` to `float` also repacks the block, and the bytes the application
        // wrote into the buffer do not move with it. Every member past the first double then
        // reads and writes at the wrong offset, and the block is simply shorter than the one
        // that was bound - the tail of it is never touched at all
        // (KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case3, whose output matched its
        // input up to the first double's slot and was zero from there on).
        //
        // The block layout is fixed by GL 4.6 core 7.6.2.2 and is asserted here as literal byte
        // offsets rather than queried, so this says what the SPEC requires and not what MobileGL
        // happens to report. Both packings are covered because they differ in exactly the places
        // that matter: std140 rounds an array's stride and a matrix's column stride up to 16,
        // std430 does not, and only std430 packs the scalars tightly.
        //
        // Every value is exactly representable in binary32, so a correct implementation copies
        // the block BYTE FOR BYTE even though it narrows each double on the way through.
        constexpr const char* kBlockCopySource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std140, binding = 0) buffer In140 {
    int    data0;
    float  data1[3];
    mat3x2 data2;
    double data3;
    double data4[2];
    int    data5;
    dvec3  data6;
} g_in140;
layout(std430, binding = 1) buffer In430 {
    int    data0;
    float  data1[3];
    mat3x2 data2;
    double data3;
    double data4[2];
    int    data5;
    dvec3  data6;
} g_in430;
layout(std140, binding = 2) buffer Out140 {
    int    data0;
    float  data1[3];
    mat3x2 data2;
    double data3;
    double data4[2];
    int    data5;
    dvec3  data6;
} g_out140;
layout(std430, binding = 3) buffer Out430 {
    int    data0;
    float  data1[3];
    mat3x2 data2;
    double data3;
    double data4[2];
    int    data5;
    dvec3  data6;
} g_out430;
void main() {
    g_out140.data0 = g_in140.data0;
    for (int i = 0; i < 3; ++i) g_out140.data1[i] = g_in140.data1[i];
    g_out140.data2 = g_in140.data2;
    g_out140.data3 = g_in140.data3;
    for (int i = 0; i < 2; ++i) g_out140.data4[i] = g_in140.data4[i];
    g_out140.data5 = g_in140.data5;
    g_out140.data6 = g_in140.data6;

    g_out430.data0 = g_in430.data0;
    for (int i = 0; i < 3; ++i) g_out430.data1[i] = g_in430.data1[i];
    g_out430.data2 = g_in430.data2;
    g_out430.data3 = g_in430.data3;
    for (int i = 0; i < 2; ++i) g_out430.data4[i] = g_in430.data4[i];
    g_out430.data5 = g_in430.data5;
    g_out430.data6 = g_in430.data6;
}
)";

        // GL 4.6 core 7.6.2.2 rule by rule, for the block above.
        //   std140: an array's element stride and a matrix's column stride round up to 16, a
        //           double aligns to 8 and a dvec3 to 32.
        //   std430: the same without the rounding - so the scalars pack tightly and only the
        //           dvec3's 32-byte alignment leaves a hole.
        struct BlockLayout {
            int data0;
            int data1;
            int data1Stride;
            int data2;
            int data2ColumnStride;
            int data3;
            int data4;
            int data4Stride;
            int data5;
            int data6;
            int size;
        };
        constexpr BlockLayout kStd140{0, 16, 16, 64, 16, 112, 128, 16, 160, 192, 216};
        constexpr BlockLayout kStd430{0, 4, 4, 16, 8, 40, 48, 8, 64, 96, 120};

        void PokeInt(std::vector<unsigned char>& bytes, int offset, int value) {
            std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
        }
        void PokeFloat(std::vector<unsigned char>& bytes, int offset, float value) {
            std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
        }
        void PokeDouble(std::vector<unsigned char>& bytes, int offset, double value) {
            std::memcpy(&bytes[static_cast<std::size_t>(offset)], &value, sizeof(value));
        }

        // The block's contents, at the offsets the standard puts them. Padding stays zero, which
        // is what makes a byte-for-byte comparison against the (zero-initialised) output buffer
        // catch a member that landed somewhere it should not have.
        std::vector<unsigned char> MakeBlockContents(const BlockLayout& layout) {
            std::vector<unsigned char> bytes(static_cast<std::size_t>(layout.size), 0);
            PokeInt(bytes, layout.data0, 1);
            for (int i = 0; i < 3; ++i) {
                PokeFloat(bytes, layout.data1 + i * layout.data1Stride, 2.0f + static_cast<float>(i));
            }
            // Column-major, two rows per column.
            for (int column = 0; column < 3; ++column) {
                for (int row = 0; row < 2; ++row) {
                    PokeFloat(bytes, layout.data2 + column * layout.data2ColumnStride + row * 4,
                              5.0f + static_cast<float>(column * 2 + row));
                }
            }
            PokeDouble(bytes, layout.data3, 11.0);
            for (int i = 0; i < 2; ++i) {
                PokeDouble(bytes, layout.data4 + i * layout.data4Stride, 12.0 + static_cast<double>(i));
            }
            PokeInt(bytes, layout.data5, 14);
            for (int i = 0; i < 3; ++i) {
                PokeDouble(bytes, layout.data6 + i * 8, 15.0 + static_cast<double>(i));
            }
            return bytes;
        }

        // Names the first byte that differs, and which member owns it, so a failure is a
        // diagnosis rather than "the buffer is wrong".
        std::string DescribeOffset(const BlockLayout& layout, int offset) {
            const std::pair<int, const char*> members[] = {
                {layout.data0, "data0"}, {layout.data1, "data1"}, {layout.data2, "data2"},
                {layout.data3, "data3"}, {layout.data4, "data4"}, {layout.data5, "data5"},
                {layout.data6, "data6"}};
            const char* owner = "(padding before data0)";
            for (const auto& [start, name] : members) {
                if (offset >= start) owner = name;
            }
            return std::string(owner);
        }

        // Every double-typed uniform shape GLSL has, all thirteen of them, in one program - the
        // shape of KHR-GL43.compute_shader.fp64-case2. The scalar and the square matrices are
        // covered by the cases above; what only a set like this reaches is the NON-SQUARE
        // matrices, whose column stride and total size both change when the demotion turns a
        // 64-bit column into a 32-bit one, and whose members therefore move every uniform
        // declared after them.
        //
        // The shader reports every component separately rather than one pass/fail flag, because
        // "the readback is wrong" is not a diagnosis: a wrong column stride, a wrong member
        // offset and a wrong narrowing all fail the same single comparison, and only the
        // component map says which.
        // No #version here on purpose: it is handed over as a separate source string, the way
        // the CTS case hands it over.
        constexpr const char* kAllDoubleShapesSource = R"(
layout(local_size_x = 1) in;
uniform double  g_0;
uniform dvec2   g_1;
uniform dvec3   g_2;
uniform dvec4   g_3;
uniform dmat2   g_4;
uniform dmat2x3 g_5;
uniform dmat2x4 g_6;
uniform dmat3x2 g_7;
uniform dmat3   g_8;
uniform dmat3x4 g_9;
uniform dmat4x2 g_10;
uniform dmat4x3 g_11;
uniform dmat4   g_12;
layout(std430, binding = 0) buffer Output {
    float g_out[];
};
void main() {
    g_out[0] = float(g_0);
    for (int i = 0; i < 2; ++i) g_out[1 + i] = float(g_1[i]);
    for (int i = 0; i < 3; ++i) g_out[3 + i] = float(g_2[i]);
    for (int i = 0; i < 4; ++i) g_out[6 + i] = float(g_3[i]);
    for (int c = 0; c < 2; ++c) for (int r = 0; r < 2; ++r) g_out[10 + c * 2 + r] = float(g_4[c][r]);
    for (int c = 0; c < 2; ++c) for (int r = 0; r < 3; ++r) g_out[14 + c * 3 + r] = float(g_5[c][r]);
    for (int c = 0; c < 2; ++c) for (int r = 0; r < 4; ++r) g_out[20 + c * 4 + r] = float(g_6[c][r]);
    for (int c = 0; c < 3; ++c) for (int r = 0; r < 2; ++r) g_out[28 + c * 2 + r] = float(g_7[c][r]);
    for (int c = 0; c < 3; ++c) for (int r = 0; r < 3; ++r) g_out[34 + c * 3 + r] = float(g_8[c][r]);
    for (int c = 0; c < 3; ++c) for (int r = 0; r < 4; ++r) g_out[43 + c * 4 + r] = float(g_9[c][r]);
    for (int c = 0; c < 4; ++c) for (int r = 0; r < 2; ++r) g_out[55 + c * 2 + r] = float(g_10[c][r]);
    for (int c = 0; c < 4; ++c) for (int r = 0; r < 3; ++r) g_out[63 + c * 3 + r] = float(g_11[c][r]);
    for (int c = 0; c < 4; ++c) for (int r = 0; r < 4; ++r) g_out[75 + c * 4 + r] = float(g_12[c][r]);
}
)";

        // The values the CTS case sets, spelled the way it spells them - column-major, and small
        // enough that every one is exact in a float. Nothing here is a precision question; a
        // component that comes back wrong came back from the wrong bytes.
        constexpr double kG0 = 1.0;
        constexpr double kG1[2] = {2.0, 3.0};
        constexpr double kG2[3] = {4.0, 5.0, 6.0};
        constexpr double kG3[4] = {7.0, 8.0, 9.0, 10.0};
        constexpr double kG4[4] = {11.0, 12.0, 13.0, 14.0};
        constexpr double kG5[6] = {15.0, 16.0, 17.0, 18.0, 19.0, 20.0};
        constexpr double kG6[8] = {21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0};
        constexpr double kG7[6] = {29.0, 30.0, 31.0, 32.0, 33.0, 34.0};
        constexpr double kG8[9] = {35.0, 36.0, 37.0, 38.0, 39.0, 40.0, 41.0, 42.0, 43.0};
        constexpr double kG9[12] = {44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0};
        constexpr double kG10[8] = {56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0};
        constexpr double kG11[12] = {63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73.0, 74.0};
        constexpr double kG12[16] = {75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0,
                                     83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0};

        struct DoubleShape {
            const char* name;
            int base;
            int columns; // 1 for the scalar and the vectors
            int rows;    // component count for the scalar and the vectors
            const double* values;
        };

        constexpr DoubleShape kDoubleShapes[] = {
            {"g_0 double", 0, 1, 1, &kG0},      {"g_1 dvec2", 1, 1, 2, kG1},
            {"g_2 dvec3", 3, 1, 3, kG2},        {"g_3 dvec4", 6, 1, 4, kG3},
            {"g_4 dmat2", 10, 2, 2, kG4},       {"g_5 dmat2x3", 14, 2, 3, kG5},
            {"g_6 dmat2x4", 20, 2, 4, kG6},     {"g_7 dmat3x2", 28, 3, 2, kG7},
            {"g_8 dmat3", 34, 3, 3, kG8},       {"g_9 dmat3x4", 43, 3, 4, kG9},
            {"g_10 dmat4x2", 55, 4, 2, kG10},   {"g_11 dmat4x3", 63, 4, 3, kG11},
            {"g_12 dmat4", 75, 4, 4, kG12},
        };

        constexpr int kAllShapeSlots = 91;

        // The conformance case's own shader, kept verbatim down to the literal suffixes and the
        // unnamed, unqualified storage block - except that each comparison sets its OWN bit
        // instead of collapsing all thirteen into one flag. That single flag is the whole reason
        // the case was unexplained for a wave: it says "something is wrong" and nothing else.
        //
        // Verbatim matters here. Reading the components out one at a time (the case above)
        // passes; whatever fails does so through the shape the conformance case actually
        // writes - whole-matrix comparison against a constructor, a storage block with no
        // layout qualifier and no instance name, values reached with constant indices.
        constexpr const char* kCtsShapedSource = R"(
layout(local_size_x = 1) in;
buffer Result {
  int g_result;
};
uniform double g_0;
uniform dvec2 g_1;
uniform dvec3 g_2;
uniform dvec4 g_3;
uniform dmat2 g_4;
uniform dmat2x3 g_5;
uniform dmat2x4 g_6;
uniform dmat3x2 g_7;
uniform dmat3 g_8;
uniform dmat3x4 g_9;
uniform dmat4x2 g_10;
uniform dmat4x3 g_11;
uniform dmat4 g_12;

void main() {
  g_result = 0;

  if (g_0 != 1.0LF) g_result |= 1;
  if (g_1 != dvec2(2.0LF, 3.0LF)) g_result |= 2;
  if (g_2 != dvec3(4.0LF, 5.0LF, 6.0LF)) g_result |= 4;
  if (g_3 != dvec4(7.0LF, 8.0LF, 9.0LF, 10.0LF)) g_result |= 8;

  if (g_4 != dmat2(11.0LF, 12.0LF, 13.0LF, 14.0LF)) g_result |= 16;
  if (g_5 != dmat2x3(15.0LF, 16.0LF, 17.0LF, 18.0LF, 19.0LF, 20.0LF)) g_result |= 32;
  if (g_6 != dmat2x4(21.0LF, 22.0LF, 23.0LF, 24.0LF, 25.0LF, 26.0LF, 27.0LF, 28.0LF)) g_result |= 64;

  if (g_7 != dmat3x2(29.0LF, 30.0LF, 31.0LF, 32.0LF, 33.0LF, 34.0LF)) g_result |= 128;
  if (g_8 != dmat3(35.0LF, 36.0LF, 37.0LF, 38.0LF, 39.0LF, 40.0LF, 41.0LF, 42.0LF, 43.0LF)) g_result |= 256;
  if (g_9 != dmat3x4(44.0LF, 45.0LF, 46.0LF, 47.0LF, 48.0LF, 49.0LF, 50.0LF, 51.0LF, 52.0LF, 53.0LF, 54.0LF, 55.0LF)) g_result |= 512;

  if (g_10 != dmat4x2(56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0)) g_result |= 1024;
  if (g_11 != dmat4x3(63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 27.0, 73, 74.0)) g_result |= 2048;
  if (g_12 != dmat4(75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0)) g_result |= 4096;
}
)";

        TEST_F(DoublePrecisionScenario, ADoubleUniformReachesTheShaderAtFloatPrecision) {
            if (!Ready()) return;
            glUseProgram(m_program);
            const GLint scalar = glGetUniformLocation(m_program, "uScalar");
            ASSERT_GE(scalar, 0);
            // 0.1 has no exact float (or double) representation, so this only passes if the
            // value really travelled through the demoted slot rather than being read out of
            // some other four bytes.
            glUniform1d(scalar, 0.1);
            glUseProgram(0);

            const std::vector<float> values = Dispatch();
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
            EXPECT_FLOAT_EQ(values[0], static_cast<float>(0.1));
            EXPECT_FLOAT_EQ(values[11], static_cast<float>(static_cast<float>(0.1) * 2.0f + 1.5f))
                << "arithmetic on the demoted value, including the folded fp64 literal";
        }

        TEST_F(DoublePrecisionScenario, EveryDoubleShapeLandsInItsOwnSlot) {
            if (!Ready()) return;
            glUseProgram(m_program);
            const GLint scalar = glGetUniformLocation(m_program, "uScalar");
            const GLint vector = glGetUniformLocation(m_program, "uVector");
            const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
            const GLint array0 = glGetUniformLocation(m_program, "uArray[0]");
            const GLint array2 = glGetUniformLocation(m_program, "uArray[2]");
            ASSERT_GE(scalar, 0);
            ASSERT_GE(vector, 0);
            ASSERT_GE(matrix, 0);
            ASSERT_GE(array0, 0);
            ASSERT_GE(array2, 0);

            glUniform1d(scalar, 5.0);
            const GLdouble vectorValue[3] = {11.0, 12.0, 13.0};
            glUniform3dv(vector, 1, vectorValue);
            // Column-major, and every entry distinct so a transposed or mis-strided write
            // cannot land on a value that happens to match.
            GLdouble matrixValue[16] = {};
            for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
            glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
            const GLdouble arrayValue[3] = {71.0, 72.0, 73.0};
            glUniform1dv(array0, 3, arrayValue);
            glUseProgram(0);

            const std::vector<float> values = Dispatch();
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
            EXPECT_FLOAT_EQ(values[0], 5.0f) << "scalar double";
            EXPECT_FLOAT_EQ(values[1], 11.0f) << "dvec3 .x";
            EXPECT_FLOAT_EQ(values[2], 12.0f) << "dvec3 .y";
            EXPECT_FLOAT_EQ(values[3], 13.0f) << "dvec3 .z";
            EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0]";
            EXPECT_FLOAT_EQ(values[5], 103.0f) << "dmat4 [0][3] - within the first column";
            EXPECT_FLOAT_EQ(values[6], 112.0f) << "dmat4 [3][0] - column stride";
            EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3]";
            EXPECT_FLOAT_EQ(values[8], 71.0f) << "double array element 0";
            EXPECT_FLOAT_EQ(values[9], 72.0f) << "double array element 1 - element stride";
            EXPECT_FLOAT_EQ(values[10], 73.0f) << "double array element 2";
        }

        TEST_F(DoublePrecisionScenario, TheTransposeFlagStillTransposes) {
            if (!Ready()) return;
            glUseProgram(m_program);
            const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
            ASSERT_GE(matrix, 0);
            GLdouble matrixValue[16] = {};
            for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
            glUniformMatrix4dv(matrix, 1, GL_TRUE, matrixValue);
            glUseProgram(0);

            const std::vector<float> values = Dispatch();
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
            // Transposed, so [column][row] now reads the source's [row][column].
            EXPECT_FLOAT_EQ(values[4], 100.0f) << "dmat4 [0][0] is on the diagonal either way";
            EXPECT_FLOAT_EQ(values[5], 112.0f) << "dmat4 [0][3] after transpose";
            EXPECT_FLOAT_EQ(values[6], 103.0f) << "dmat4 [3][0] after transpose";
            EXPECT_FLOAT_EQ(values[7], 115.0f) << "dmat4 [3][3] is on the diagonal either way";
        }

        TEST_F(DoublePrecisionScenario, TheUniformIsStillReportedAsADouble) {
            if (!Ready()) return;
            // The demotion is an implementation detail of how the value is STORED. What the
            // shader source declared is what the application asked about, so the reflection
            // keeps answering GL_DOUBLE* - an application that switches on the type and calls
            // glUniform*d has to keep working, and it is the glUniform*d path that is correct
            // for these uniforms.
            struct Expectation {
                const char* name;
                GLenum type;
                GLint size;
            };
            const Expectation expectations[] = {
                {"uScalar", GL_DOUBLE, 1},
                {"uVector", GL_DOUBLE_VEC3, 1},
                {"uMatrix", GL_DOUBLE_MAT4, 1},
                {"uArray[0]", GL_DOUBLE, 3},
            };

            GLint activeUniforms = 0;
            glGetProgramiv(m_program, GL_ACTIVE_UNIFORMS, &activeUniforms);
            ASSERT_GT(activeUniforms, 0);

            for (const Expectation& expectation : expectations) {
                bool found = false;
                for (GLint index = 0; index < activeUniforms; ++index) {
                    char name[128] = {};
                    GLsizei length = 0;
                    GLint size = 0;
                    GLenum type = 0;
                    glGetActiveUniform(m_program, static_cast<GLuint>(index), sizeof(name) - 1, &length, &size,
                                       &type, name);
                    if (std::string(name, static_cast<size_t>(length)) != expectation.name) continue;
                    found = true;
                    EXPECT_EQ(type, expectation.type) << expectation.name;
                    EXPECT_EQ(size, expectation.size) << expectation.name;
                    break;
                }
                EXPECT_TRUE(found) << "glGetActiveUniform never reported " << expectation.name;
            }
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
        }

        TEST_F(DoublePrecisionScenario, GetUniformdvReadsBackWhatWasStored) {
            if (!Ready()) return;
            glUseProgram(m_program);
            const GLint scalar = glGetUniformLocation(m_program, "uScalar");
            const GLint vector = glGetUniformLocation(m_program, "uVector");
            const GLint matrix = glGetUniformLocation(m_program, "uMatrix");
            ASSERT_GE(scalar, 0);
            ASSERT_GE(vector, 0);
            ASSERT_GE(matrix, 0);
            glUniform1d(scalar, 0.1);
            const GLdouble vectorValue[3] = {11.5, 12.5, 13.5};
            glUniform3dv(vector, 1, vectorValue);
            GLdouble matrixValue[16] = {};
            for (int i = 0; i < 16; ++i) matrixValue[i] = 100.0 + i;
            glUniformMatrix4dv(matrix, 1, GL_FALSE, matrixValue);
            glUseProgram(0);

            // The readback has to undo exactly what the write did - the same std140 column
            // padding, the same component width - or a dmat4 comes back with its columns
            // shifted and nothing else in the API would say so. Every value below except the
            // scalar is exact in float32, so those expectations pin the LAYOUT and hold in
            // either regime; the scalar is the one that also pins the PRECISION.
            GLdouble readScalar = 0.0;
            glGetUniformdv(m_program, scalar, &readScalar);
            // 0.1 is not representable in float32, so what comes back names the regime: a
            // backend without native fp64 narrowed it at the glUniform1d above (the module's own
            // doubles were demoted, so its storage is 4 bytes per component), and one with it
            // stored the double whole. Both are correct; asserting only the narrow answer would
            // fail the moment fp64 stops being emulated, and asserting only the wide one would
            // fail on every mobile device there is.
            if (readScalar == 0.1) {
                SUCCEED() << "this backend consumes 64-bit floats natively; the double survived whole";
            } else {
                EXPECT_DOUBLE_EQ(readScalar, static_cast<double>(static_cast<float>(0.1)))
                    << "the value is what a float can hold, not the double that was passed in";
            }

            GLdouble readVector[3] = {};
            glGetUniformdv(m_program, vector, readVector);
            EXPECT_DOUBLE_EQ(readVector[0], 11.5);
            EXPECT_DOUBLE_EQ(readVector[1], 12.5);
            EXPECT_DOUBLE_EQ(readVector[2], 13.5);

            GLdouble readMatrix[16] = {};
            glGetUniformdv(m_program, matrix, readMatrix);
            for (int i = 0; i < 16; ++i) {
                EXPECT_DOUBLE_EQ(readMatrix[i], 100.0 + i) << "dmat4 component " << i;
            }

            // The float query sees the same storage through a narrower type, and answers the
            // same float either way: GL 4.6 core 7.6 converts on the way out.
            GLfloat readFloat = 0.0f;
            glGetUniformfv(m_program, scalar, &readFloat);
            EXPECT_FLOAT_EQ(readFloat, static_cast<float>(0.1));
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
        }

        TEST_F(DoublePrecisionScenario, ADoubleUniformKeepsItsDeclaredInitializer) {
            if (!Ready()) return;
            // A declared initializer is seeded straight into the uniform shadow at link, and the
            // seeding used to skip 64-bit floats outright ("no 32-bit shadow encoding") - which
            // was true before the demotion and silently left every such uniform reading zero.
            const char* source = R"(#version 430 core
layout(local_size_x = 1) in;
uniform double uSeeded = 2.5lf;
uniform dvec3  uSeededVector = dvec3(4.0lf, 5.0lf, 6.0lf);
layout(std430, binding = 0) buffer Output {
    float g_out[];
};
void main() {
    g_out[0] = float(uSeeded);
    g_out[1] = float(uSeededVector.x);
    g_out[2] = float(uSeededVector.y);
    g_out[3] = float(uSeededVector.z);
}
)";
            const GLuint program = CompileComputeProgram(source);
            ASSERT_NE(program, 0u) << m_buildLog;

            glUseProgram(program);
            glDispatchCompute(1, 1, 1);
            glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
            std::vector<float> values(4, -1.0f);
            glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_output);
            glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, 4 * sizeof(float), values.data());
            glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
            glUseProgram(0);
            glDeleteProgram(program);

            EXPECT_FLOAT_EQ(values[0], 2.5f) << "scalar double initializer";
            EXPECT_FLOAT_EQ(values[1], 4.0f) << "dvec3 initializer .x";
            EXPECT_FLOAT_EQ(values[2], 5.0f) << "dvec3 initializer .y";
            EXPECT_FLOAT_EQ(values[3], 6.0f) << "dvec3 initializer .z";
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
        }

        TEST_F(DoublePrecisionScenario, EveryDoubleUniformShapeArrivesWhereTheShaderReadsIt) {
            if (!Ready()) return;
            // Built the way the CTS case builds it, because every step of that build has been a
            // bug here at least once: the source arrives as TWO strings (the version directive
            // and the body), the shader is attached before it has a source and deleted while
            // still attached, and the program is linked twice.
            m_shapeProgram = glCreateProgram();
            ASSERT_NE(m_shapeProgram, 0u);
            {
                const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
                glAttachShader(m_shapeProgram, shader);
                glDeleteShader(shader);
                const char* const sources[2] = {"#version 430 core\n", kAllDoubleShapesSource};
                glShaderSource(shader, 2, sources, nullptr);
                glCompileShader(shader);
                GLint compiled = 0;
                glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
                if (compiled == GL_FALSE) {
                    char log[2048] = {};
                    glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
                    FAIL() << "compute shader did not compile: " << log;
                }
            }
            glLinkProgram(m_shapeProgram);
            {
                GLint linkedOnce = 0;
                glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linkedOnce);
                if (linkedOnce == GL_FALSE) {
                    char log[2048] = {};
                    glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
                    FAIL() << "compute program did not link: " << log;
                }
            }

            glGenBuffers(1, &m_shapeOutput);
            glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
            const std::vector<float> zeroes(kAllShapeSlots, 0.0f);
            glBufferData(GL_SHADER_STORAGE_BUFFER, kAllShapeSlots * sizeof(float), zeroes.data(), GL_DYNAMIC_DRAW);
            glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
            glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);

            const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };

            // Pass one sets through glProgramUniform*, pass two through glUniform* after a
            // re-link - the two entry-point families the CTS case exercises, and two different
            // routes into the same uniform storage.
            const auto setWithProgramUniform = [&]() {
                glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
                glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
                glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
                glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
                glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
                glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
                glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
                glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
                glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
                glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
                glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
                glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
                glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);
            };
            // Deliberately does NOT re-issue glUseProgram: the CTS case leaves the program
            // current across the re-link and writes into it from there, so this is the path
            // where a re-link has to keep the current program's uniform storage addressable.
            const auto setWithUniform = [&]() {
                glUniform1d(location("g_0"), kG0);
                glUniform2d(location("g_1"), kG1[0], kG1[1]);
                glUniform3d(location("g_2"), kG2[0], kG2[1], kG2[2]);
                glUniform4d(location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
                glUniformMatrix2dv(location("g_4"), 1, GL_FALSE, kG4);
                glUniformMatrix2x3dv(location("g_5"), 1, GL_FALSE, kG5);
                glUniformMatrix2x4dv(location("g_6"), 1, GL_FALSE, kG6);
                glUniformMatrix3x2dv(location("g_7"), 1, GL_FALSE, kG7);
                glUniformMatrix3dv(location("g_8"), 1, GL_FALSE, kG8);
                glUniformMatrix3x4dv(location("g_9"), 1, GL_FALSE, kG9);
                glUniformMatrix4x2dv(location("g_10"), 1, GL_FALSE, kG10);
                glUniformMatrix4x3dv(location("g_11"), 1, GL_FALSE, kG11);
                glUniformMatrix4dv(location("g_12"), 1, GL_FALSE, kG12);
            };

            const auto dispatchAndRead = [&]() {
                glUseProgram(m_shapeProgram);
                glDispatchCompute(1, 1, 1);
                glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
                std::vector<float> values(kAllShapeSlots, -1.0f);
                glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
                glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), values.data());
                glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
                // The program stays current on purpose - see setWithUniform.
                return values;
            };

            const auto expectEverything = [](const std::vector<float>& values, const char* pass) {
                for (const DoubleShape& shape : kDoubleShapes) {
                    for (int c = 0; c < shape.columns; ++c) {
                        for (int r = 0; r < shape.rows; ++r) {
                            const int component = c * shape.rows + r;
                            EXPECT_FLOAT_EQ(values[shape.base + component],
                                            static_cast<float>(shape.values[component]))
                                << pass << ": " << shape.name << " column " << c << " row " << r;
                        }
                    }
                }
            };

            setWithProgramUniform();
            expectEverything(dispatchAndRead(), "glProgramUniform*");

            // A re-link zeroes every uniform, so pass two proves its own writes rather than
            // reading pass one's bytes back.
            glLinkProgram(m_shapeProgram);
            GLint linked = 0;
            glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
            ASSERT_EQ(linked, GL_TRUE);
            glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_shapeOutput);
            glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, kAllShapeSlots * sizeof(float), zeroes.data());
            glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);

            setWithUniform();
            expectEverything(dispatchAndRead(), "glUniform* after re-link");
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
        }

        TEST_F(DoublePrecisionScenario, TheConformanceUniformShaderAgreesWithEveryValueItWasGiven) {
            if (!Ready()) return;
            m_shapeProgram = glCreateProgram();
            ASSERT_NE(m_shapeProgram, 0u);
            {
                const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
                glAttachShader(m_shapeProgram, shader);
                glDeleteShader(shader);
                const char* const sources[2] = {"#version 430 core\n", kCtsShapedSource};
                glShaderSource(shader, 2, sources, nullptr);
                glCompileShader(shader);
                GLint compiled = 0;
                glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
                if (compiled == GL_FALSE) {
                    char log[2048] = {};
                    glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
                    FAIL() << "compute shader did not compile: " << log;
                }
            }
            glLinkProgram(m_shapeProgram);
            GLint linked = 0;
            glGetProgramiv(m_shapeProgram, GL_LINK_STATUS, &linked);
            if (linked == GL_FALSE) {
                char log[2048] = {};
                glGetProgramInfoLog(m_shapeProgram, sizeof(log) - 1, nullptr, log);
                FAIL() << "compute program did not link: " << log;
            }

            glGenBuffers(1, &m_shapeOutput);
            const GLint seed = 123;
            glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_shapeOutput);
            glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(seed), &seed, GL_STATIC_DRAW);

            const auto location = [&](const char* name) { return glGetUniformLocation(m_shapeProgram, name); };
            glProgramUniform1d(m_shapeProgram, location("g_0"), kG0);
            glProgramUniform2d(m_shapeProgram, location("g_1"), kG1[0], kG1[1]);
            glProgramUniform3d(m_shapeProgram, location("g_2"), kG2[0], kG2[1], kG2[2]);
            glProgramUniform4d(m_shapeProgram, location("g_3"), kG3[0], kG3[1], kG3[2], kG3[3]);
            glProgramUniformMatrix2dv(m_shapeProgram, location("g_4"), 1, GL_FALSE, kG4);
            glProgramUniformMatrix2x3dv(m_shapeProgram, location("g_5"), 1, GL_FALSE, kG5);
            glProgramUniformMatrix2x4dv(m_shapeProgram, location("g_6"), 1, GL_FALSE, kG6);
            glProgramUniformMatrix3x2dv(m_shapeProgram, location("g_7"), 1, GL_FALSE, kG7);
            glProgramUniformMatrix3dv(m_shapeProgram, location("g_8"), 1, GL_FALSE, kG8);
            glProgramUniformMatrix3x4dv(m_shapeProgram, location("g_9"), 1, GL_FALSE, kG9);
            glProgramUniformMatrix4x2dv(m_shapeProgram, location("g_10"), 1, GL_FALSE, kG10);
            glProgramUniformMatrix4x3dv(m_shapeProgram, location("g_11"), 1, GL_FALSE, kG11);
            glProgramUniformMatrix4dv(m_shapeProgram, location("g_12"), 1, GL_FALSE, kG12);

            glUseProgram(m_shapeProgram);
            glDispatchCompute(1, 1, 1);
            glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);

            GLint disagreements = -1;
            glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(disagreements), &disagreements);
            for (int bit = 0; bit < 13; ++bit) {
                EXPECT_EQ(disagreements & (1 << bit), 0)
                    << kDoubleShapes[bit].name << " did not compare equal to the value it was given";
            }
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
        }

        TEST_F(DoublePrecisionScenario, TheFp64ExtensionIsNotAdvertised) {
            if (!Ready()) return;
            // The shader above compiled, linked and ran without the extension string, which is
            // the point: an application does not need GL_ARB_gpu_shader_fp64 advertised to USE
            // doubles here. What the string additionally promises is 64-bit precision, and that
            // is the one thing the demotion cannot deliver - so it stays off unless
            // MOBILEGL_ADVERTISE_FP64 asks for it, and an application that branches on the
            // string keeps taking its float path.
            GLint extensionCount = 0;
            glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
            ASSERT_GT(extensionCount, 0);
            bool advertised = false;
            for (GLint i = 0; i < extensionCount; ++i) {
                const char* name = reinterpret_cast<const char*>(glGetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
                if (name != nullptr && std::string(name) == "GL_ARB_gpu_shader_fp64") advertised = true;
            }
            EXPECT_FALSE(advertised);
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
        }

        TEST_F(DoublePrecisionScenario, A64BitVertexFormatIsRecordedAndItsArrayIsDroppedAtDraw) {
            if (!Ready()) return;
            // The demotion leaves no 64-bit shader input to feed, so there is nothing a 64-bit
            // vertex FETCH could be fetched into - on either backend, and no longer only on the
            // ones whose device lacks shaderFloat64.
            //
            // What that costs is the ARRAY, not the CALL. GL 4.6 core 10.3.2 defines no error for
            // a well-formed glVertexAttribLFormat and 64-bit attributes are core in the GL 4.3
            // context MobileGL advertises, so refusing the call would be non-conformant and would
            // leave four pure state queries unanswerable
            // (KHR-GL43.vertex_attrib_binding.basic-state1/3). The format is therefore recorded and
            // queryable; the enabled array is what gets dropped, and the attribute then reads its
            // generic current value. The matching POST row says exactly that at startup.
            GLuint vao = 0;
            glGenVertexArrays(1, &vao);
            glBindVertexArray(vao);
            while (glGetError() != GL_NO_ERROR) {}

            glVertexAttribLFormat(1, 3, GL_DOUBLE, 8);
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
                << "glVertexAttribLFormat is a legal call in a GL 4.3 context";

            GLint attribSize = 0;
            GLint attribType = 0;
            GLint attribIsLong = 0;
            GLint attribRelativeOffset = 0;
            glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_SIZE, &attribSize);
            glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_TYPE, &attribType);
            glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_ARRAY_LONG, &attribIsLong);
            glGetVertexAttribiv(1, GL_VERTEX_ATTRIB_RELATIVE_OFFSET, &attribRelativeOffset);
            EXPECT_EQ(attribSize, 3);
            EXPECT_EQ(attribType, static_cast<GLint>(GL_DOUBLE));
            EXPECT_EQ(attribIsLong, GL_TRUE) << "GL_VERTEX_ATTRIB_ARRAY_LONG is what makes this the "
                                                "unconverted form; without it the state is a lie";
            EXPECT_EQ(attribRelativeOffset, 8);
            EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));

            glBindVertexArray(0);
            glDeleteVertexArrays(1, &vao);
            while (glGetError() != GL_NO_ERROR) {}
        }

        // The consequence of recording the state rather than refusing the call: a 64-bit array can
        // now be ENABLED in a VAO that a draw uses, which it never could before. That must not
        // take the draw down. Leaving such an array enabled with no pointer behind it is exactly
        // the documented Adreno null-deref (SIGSEGV inside the next glDraw*), so DirectGLES
        // disables it before glVertexAttribPointer can ever see GL_DOUBLE, and DirectVulkan maps
        // the format to VK_FORMAT_UNDEFINED so it never enters the pipeline's vertex input state.
        //
        // The shader deliberately does NOT read location 1: that keeps the two backends on the
        // same path (DirectVulkan declines a draw whose SHADER reads an unsupported enabled array,
        // by design and loudly, which is a different assertion from this one) and it is the shape
        // the crash needed - an enabled array nothing set a pointer for.
        TEST_F(DoublePrecisionScenario, AnEnabledLongArrayDoesNotBreakADrawThatIgnoresIt) {
            if (!Ready()) return;

            constexpr const char* kVs = R"(#version 430 core
layout(location = 0) in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
            constexpr const char* kFs = R"(#version 430 core
out vec4 o_color;
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
)";
            std::string error;
            const unsigned int program = CompileProgram(kVs, kFs, &error);
            ASSERT_NE(program, 0u) << error;

            ColorFbo target = MakeColorFbo(32, 32);
            ASSERT_NE(target.fbo, 0u) << "could not create the render target";
            BindFbo(target);

            const float positions[8] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
            const double doubles[4] = {1.0, 2.0, 3.0, 4.0};

            GLuint vao = 0;
            GLuint positionBuffer = 0;
            GLuint doubleBuffer = 0;
            glGenVertexArrays(1, &vao);
            glBindVertexArray(vao);
            glGenBuffers(1, &positionBuffer);
            glBindBuffer(GL_ARRAY_BUFFER, positionBuffer);
            glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
            glGenBuffers(1, &doubleBuffer);
            glBindBuffer(GL_ARRAY_BUFFER, doubleBuffer);
            glBufferData(GL_ARRAY_BUFFER, sizeof(doubles), doubles, GL_STATIC_DRAW);
            glBindBuffer(GL_ARRAY_BUFFER, 0);

            glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
            glVertexAttribBinding(0, 0);
            glBindVertexBuffer(0, positionBuffer, 0, static_cast<GLsizei>(2 * sizeof(float)));
            glEnableVertexAttribArray(0);

            glVertexAttribLFormat(1, 1, GL_DOUBLE, 0);
            glVertexAttribBinding(1, 1);
            glBindVertexBuffer(1, doubleBuffer, 0, static_cast<GLsizei>(sizeof(double)));
            glEnableVertexAttribArray(1);
            EXPECT_EQ(FirstGLError(), 0u) << "setting up the 64-bit array was refused";

            ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
            glUseProgram(program);
            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
            EXPECT_EQ(FirstGLError(), 0u) << "a draw with an enabled 64-bit array must not raise an error";

            const Image image = ReadPixels(target.width, target.height);
            ASSERT_FALSE(image.Empty());
            EXPECT_GT(image.At(target.width / 2, target.height / 2).g, 200)
                << "the draw did not happen; the enabled 64-bit array must be dropped, not fatal";

            glDisableVertexAttribArray(0);
            glDisableVertexAttribArray(1);
            glBindVertexArray(0);
            glDeleteVertexArrays(1, &vao);
            glDeleteBuffers(1, &positionBuffer);
            glDeleteBuffers(1, &doubleBuffer);
            BindDefaultFramebuffer();
            DestroyColorFbo(target);
            glUseProgram(0);
            glDeleteProgram(program);
            EXPECT_EQ(FirstGLError(), 0u);
        }

        TEST_F(DoublePrecisionScenario, AStorageBlockWithDoublesKeepsTheLayoutItWasBoundWith) {
            if (!Ready()) return;

            GLint blocks = 0;
            glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &blocks);
            if (blocks < 4) {
                GTEST_SKIP() << "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS is " << blocks << "; this needs 4";
            }

            const unsigned int program = CompileComputeProgram(kBlockCopySource);
            ASSERT_NE(program, 0u) << m_buildLog;

            const std::vector<unsigned char> in140 = MakeBlockContents(kStd140);
            const std::vector<unsigned char> in430 = MakeBlockContents(kStd430);
            const std::vector<unsigned char> zero140(in140.size(), 0);
            const std::vector<unsigned char> zero430(in430.size(), 0);

            GLuint buffers[4] = {};
            glGenBuffers(4, buffers);
            const std::vector<unsigned char>* contents[4] = {&in140, &in430, &zero140, &zero430};
            for (int i = 0; i < 4; ++i) {
                glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast<GLuint>(i), buffers[i]);
                glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast<GLsizeiptr>(contents[i]->size()),
                             contents[i]->data(), GL_DYNAMIC_COPY);
            }
            ASSERT_EQ(FirstGLError(), 0u);

            glUseProgram(program);
            glDispatchCompute(1, 1, 1);
            glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
            EXPECT_EQ(FirstGLError(), 0u);

            for (int pass = 0; pass < 2; ++pass) {
                const BlockLayout& layout = pass == 0 ? kStd140 : kStd430;
                const std::vector<unsigned char>& expected = pass == 0 ? in140 : in430;
                const char* packing = pass == 0 ? "std140" : "std430";
                std::vector<unsigned char> observed(expected.size(), 0xEE);
                glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[2 + pass]);
                glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
                                   static_cast<GLsizeiptr>(observed.size()), observed.data());
                int mismatches = 0;
                int firstMismatch = -1;
                for (std::size_t i = 0; i < expected.size(); ++i) {
                    if (expected[i] == observed[i]) continue;
                    ++mismatches;
                    if (firstMismatch < 0) firstMismatch = static_cast<int>(i);
                }
                EXPECT_EQ(mismatches, 0)
                    << packing << " block: " << mismatches << " of " << expected.size()
                    << " bytes differ, first at byte " << firstMismatch << " (in "
                    << DescribeOffset(layout, firstMismatch < 0 ? 0 : firstMismatch)
                    << "); a block that was repacked around its doubles reads and writes every "
                       "member after the first one at the wrong offset";
            }

            glUseProgram(0);
            glDeleteProgram(program);
            glDeleteBuffers(4, buffers);
            EXPECT_EQ(FirstGLError(), 0u);
        }

    } // namespace
} // namespace MGITest
