// 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 #include #include #include #include "../Harness/HeadlessGL.h" #include "../Harness/ScenarioFixture.h" #ifdef GLAPI #undef GLAPI #endif #define GL_GLEXT_PROTOTYPES #include #include #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 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 Dispatch() { glUseProgram(m_program); glDispatchCompute(1, 1, 1); glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); std::vector 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& bytes, int offset, int value) { std::memcpy(&bytes[static_cast(offset)], &value, sizeof(value)); } void PokeFloat(std::vector& bytes, int offset, float value) { std::memcpy(&bytes[static_cast(offset)], &value, sizeof(value)); } void PokeDouble(std::vector& bytes, int offset, double value) { std::memcpy(&bytes[static_cast(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 MakeBlockContents(const BlockLayout& layout) { std::vector bytes(static_cast(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(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(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(i)); } PokeInt(bytes, layout.data5, 14); for (int i = 0; i < 3; ++i) { PokeDouble(bytes, layout.data6 + i * 8, 15.0 + static_cast(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 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 values = Dispatch(); EXPECT_EQ(glGetError(), static_cast(GL_NO_ERROR)); EXPECT_FLOAT_EQ(values[0], static_cast(0.1)); EXPECT_FLOAT_EQ(values[11], static_cast(static_cast(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 values = Dispatch(); EXPECT_EQ(glGetError(), static_cast(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 values = Dispatch(); EXPECT_EQ(glGetError(), static_cast(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(index), sizeof(name) - 1, &length, &size, &type, name); if (std::string(name, static_cast(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(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(static_cast(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(0.1)); EXPECT_EQ(glGetError(), static_cast(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 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(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 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 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& 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(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(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(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(glGetStringi(GL_EXTENSIONS, static_cast(i))); if (name != nullptr && std::string(name) == "GL_ARB_gpu_shader_fp64") advertised = true; } EXPECT_FALSE(advertised); EXPECT_EQ(glGetError(), static_cast(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(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(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(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(2 * sizeof(float))); glEnableVertexAttribArray(0); glVertexAttribLFormat(1, 1, GL_DOUBLE, 0); glVertexAttribBinding(1, 1); glBindVertexBuffer(1, doubleBuffer, 0, static_cast(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 in140 = MakeBlockContents(kStd140); const std::vector in430 = MakeBlockContents(kStd430); const std::vector zero140(in140.size(), 0); const std::vector zero430(in430.size(), 0); GLuint buffers[4] = {}; glGenBuffers(4, buffers); const std::vector* contents[4] = {&in140, &in430, &zero140, &zero430}; for (int i = 0; i < 4; ++i) { glBindBufferBase(GL_SHADER_STORAGE_BUFFER, static_cast(i), buffers[i]); glBufferData(GL_SHADER_STORAGE_BUFFER, static_cast(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& expected = pass == 0 ? in140 : in430; const char* packing = pass == 0 ? "std140" : "std430"; std::vector observed(expected.size(), 0xEE); glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffers[2 + pass]); glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, static_cast(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(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