mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
721 lines
36 KiB
C++
721 lines
36 KiB
C++
// 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, RUN AT SINGLE PRECISION.
|
|
//
|
|
// 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, 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. MobileGL therefore 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.
|
|
//
|
|
// The narrowing is only half a contract. The other half is the API side: the global UBO is
|
|
// laid out by reflecting the DEMOTED module, so glUniform*d has to store a float where the
|
|
// shader reads a float, glGetUniform*v has to read one back, and a dmat4's columns are now
|
|
// std140-padded like any other matrix's. 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.
|
|
//
|
|
// What is deliberately NOT asserted: that the values are exact to double precision. They are
|
|
// not, and cannot be. Every expectation here is the float value of the double that was set,
|
|
// which is the whole point.
|
|
|
|
#include <cmath>
|
|
#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;
|
|
};
|
|
|
|
// 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 4-byte components - or a dmat4 comes back with its columns
|
|
// shifted and nothing else in the API would say so.
|
|
GLdouble readScalar = 0.0;
|
|
glGetUniformdv(m_program, scalar, &readScalar);
|
|
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 the type it is actually stored as.
|
|
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, A64BitVertexFormatIsDeclinedOnEveryBackend) {
|
|
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. Declined loudly rather than accepted and
|
|
// drawn as garbage; the matching POST row says the same thing at startup.
|
|
GLuint vao = 0;
|
|
glGenVertexArrays(1, &vao);
|
|
glBindVertexArray(vao);
|
|
while (glGetError() != GL_NO_ERROR) {}
|
|
|
|
glVertexAttribLFormat(0, 3, GL_DOUBLE, 0);
|
|
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
|
|
|
|
glBindVertexArray(0);
|
|
glDeleteVertexArrays(1, &vao);
|
|
while (glGetError() != GL_NO_ERROR) {}
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace MGITest
|