diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index d02e2956..eeea664b 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -909,7 +909,13 @@ namespace MobileGL::MG_Impl::GLImpl { } if (!TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) { - Memcpy(params, pUBO + offset, size); + // Never more than the uniform actually occupies. `size` is the GL type size, + // which for a `double` uniform is twice its storage - every 64-bit float is + // narrowed before the module reaches a backend, so the slot holds floats. The + // typed entry points (glGetUniformdv and friends) go through + // GetUniformScalar_State, which converts component by component; this raw + // copy has no type to convert with, so it is bounded rather than converted. + Memcpy(params, pUBO + offset, std::min(size, span)); } } // TODO: handle 1i variant as texture unit @@ -960,22 +966,27 @@ namespace MobileGL::MG_Impl::GLImpl { if (TryGatherFloatMatrixColumns(ttype, pUBO + offset, params)) return; } - // A double-precision uniform is the one case where the stored component type can - // differ from the queried one for a non-opaque uniform, and the difference is not - // just a reinterpretation: it is twice as wide, so a raw copy would overrun the - // caller's buffer as well as return nonsense. Read component by component and let - // GL's conversion rules (7.6: round to nearest for the integer queries) apply. + // A double-precision uniform is the one case where the stored component type differs + // from the DECLARED one for a non-opaque uniform: the shader's 64-bit floats are + // narrowed to 32 bits before the module reaches a backend + // (ShaderTranspiler::DemoteFloat64Pass), so what is in the global UBO is a float per + // component, laid out exactly like the float-typed twin of this uniform - std140 + // 16-byte column stride for a matrix included. Reading it as a GLdouble would return + // two components reinterpreted as one. Read component by component and let GL's + // conversion rules (7.6: round to nearest for the integer queries) apply; the value + // widens back to the queried type, having lost precision at the glUniform*d that + // stored it and not here. if (ttype->getBasicType() == glslang::EbtDouble) { const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1; const Int rows = ttype->isMatrix() ? ttype->getMatrixRows() : (ttype->isVector() ? ttype->getVectorSize() : 1); - // The slot the linker handed out is exactly `columns` columns wide, so it also - // states the column stride - which for a double matrix is not a float's 16 bytes. - const SizeT columnStride = columns > 0 ? size / static_cast(columns) : size; + // std140 gives every matrix column its own 16-byte slot; a non-matrix is one + // tightly packed run and never reaches the stride at all. + const SizeT columnStride = 4 * sizeof(GLfloat); for (Int column = 0; column < columns; ++column) { for (Int row = 0; row < rows; ++row) { - GLdouble component = 0.0; - Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLdouble), + GLfloat component = 0.0f; + Memcpy(&component, pUBO + offset + column * columnStride + row * sizeof(GLfloat), sizeof(component)); if constexpr (std::is_integral_v) { // Rounded to the nearest integer and clamped into the queried type's @@ -1248,36 +1259,39 @@ namespace MobileGL::MG_Impl::GLImpl { } } - // glUniform*d / glUniformMatrix*dv. The vector forms need nothing beyond the shared - // upload template - it is already typed on the component - but a matrix does: the - // column stride the linker used for a double matrix is not the 16 bytes a float one - // gets. It is not guessed here; the slot the uniform was given is exactly `columns` - // columns wide, so dividing states the stride the rest of the pipeline agreed on. - template - void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose, - const GLdouble* value, Int columns, Int rows) { - const SizeT slotSize = programObject.GetUniformSizesInBytes(location); - const SizeT columnStride = columns > 0 ? slotSize / static_cast(columns) : slotSize; - const SizeT componentCount = static_cast(columns) * static_cast(rows); - Vector column(static_cast(rows)); - for (GLint matrix = 0; matrix < count; ++matrix) { - if (matrix > 0 && !programObject.UniformLocationsAliasSameUniform(location, location + matrix)) break; - if (!programObject.IsValidUniformLocation(location + matrix)) { - RecordInvalidUniformLocationError(__func__, location + matrix, "the current program object"); - return; - } - const GLdouble* source = value + matrix * componentCount; - for (Int c = 0; c < columns; ++c) { - for (Int r = 0; r < rows; ++r) { - column[r] = transpose == GL_TRUE ? source[r * columns + c] : source[c * rows + r]; - } - Uniform_State<1>(programObject, location + matrix, column.data(), c * columnStride); - for (Int r = 1; r < rows; ++r) { - Uniform_State<1>(programObject, location + matrix, column.data() + r, - c * columnStride + r * sizeof(GLdouble)); - } - } + // glUniform*d / glUniformMatrix*dv. Neither needs a layout of its own any more: the + // transpile chain narrows every 64-bit float in the shader to 32 bits + // (ShaderTranspiler::DemoteFloat64Pass) and the global UBO is laid out by reflecting that + // demoted module, so a double uniform's storage IS a float uniform's - same offset, same + // 4-byte components, same std140 column padding for matrices. Narrowing here, at the one + // place the 64-bit value enters, and then handing the bytes to the ordinary float upload + // path is what keeps the two in step; a separate double-shaped layout here would write + // 8-byte components into 4-byte slots and silently address the wrong ones. + // + // The narrowing is the same static_cast the shader's own arithmetic now performs, so the + // value the shader reads is the value glUniform*d was given, at float precision. + template + void UniformvNarrowed_State(GLint location, GLsizei count, const GLdouble* value) { + if (value == nullptr || count <= 0) { + // Same shape as the float entry points: the location validation still runs, and a + // null pointer is left to fault exactly where glUniform*fv would. + Uniformv_State(location, count, reinterpret_cast(value)); + return; } + Vector narrowed(static_cast(count) * ItemCount); + for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast(value[i]); + Uniformv_State(location, count, narrowed.data()); + } + + template + void ProgramUniformvNarrowed_State(GLuint program, GLint location, GLsizei count, const GLdouble* value) { + if (value == nullptr || count <= 0) { + ProgramUniformv_State(program, location, count, reinterpret_cast(value)); + return; + } + Vector narrowed(static_cast(count) * ItemCount); + for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast(value[i]); + ProgramUniformv_State(program, location, count, narrowed.data()); } // glUniformMatrix*fv / glProgramUniformMatrix*fv, every shape (square and non-square). @@ -1326,6 +1340,22 @@ namespace MobileGL::MG_Impl::GLImpl { } } + // glUniformMatrix*dv / glProgramUniformMatrix*dv. Narrowed to the float form and handed + // straight to it: after DemoteFloat64Pass a `dmat4` uniform is a `mat4` in the shader and a + // mat4-shaped slot in the global UBO, columns padded to a vec4 and all. Everything else + // about the call - transpose handling, the array-element walk, the opaque-uniform refusal - + // is then the one implementation both spellings share. + template + void UniformMatrixdv_Object(Program& programObject, GLint location, GLsizei count, GLboolean transpose, + const GLdouble* value, Int columns, Int rows) { + if (value == nullptr || count <= 0) return; + const SizeT componentCount = static_cast(columns) * static_cast(rows); + Vector narrowed(static_cast(count) * componentCount); + for (SizeT i = 0; i < narrowed.size(); ++i) narrowed[i] = static_cast(value[i]); + UniformMatrixfv_Object(programObject, "glUniformMatrixdv", location, count, transpose, narrowed.data(), + columns, rows, "the current program object"); + } + // Helper function to transpose a 2x2 matrix void TransposeMatrix2x2(const GLfloat* input, GLfloat* output) { // Input matrix is in column-major order (OpenGL default) @@ -2089,71 +2119,71 @@ namespace MobileGL::MG_Impl::GLImpl { } void Uniform1d(GLint location, GLdouble v0) { const GLdouble v[] = {v0}; - Uniformv_State<1>(location, 1, v); + UniformvNarrowed_State<1>(location, 1, v); } void Uniform1dv(GLint location, GLsizei count, const GLdouble* value) { - Uniformv_State<1>(location, count, value); + UniformvNarrowed_State<1>(location, count, value); } void ProgramUniform1d(GLuint program, GLint location, GLdouble v0) { const GLdouble v[] = {v0}; - ProgramUniformv_State<1>(program, location, 1, v); + ProgramUniformvNarrowed_State<1>(program, location, 1, v); } void ProgramUniform1dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) { - ProgramUniformv_State<1>(program, location, count, value); + ProgramUniformvNarrowed_State<1>(program, location, count, value); } void Uniform2d(GLint location, GLdouble v0, GLdouble v1) { const GLdouble v[] = {v0, v1}; - Uniformv_State<2>(location, 1, v); + UniformvNarrowed_State<2>(location, 1, v); } void Uniform2dv(GLint location, GLsizei count, const GLdouble* value) { - Uniformv_State<2>(location, count, value); + UniformvNarrowed_State<2>(location, count, value); } void ProgramUniform2d(GLuint program, GLint location, GLdouble v0, GLdouble v1) { const GLdouble v[] = {v0, v1}; - ProgramUniformv_State<2>(program, location, 1, v); + ProgramUniformvNarrowed_State<2>(program, location, 1, v); } void ProgramUniform2dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) { - ProgramUniformv_State<2>(program, location, count, value); + ProgramUniformvNarrowed_State<2>(program, location, count, value); } void Uniform3d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2) { const GLdouble v[] = {v0, v1, v2}; - Uniformv_State<3>(location, 1, v); + UniformvNarrowed_State<3>(location, 1, v); } void Uniform3dv(GLint location, GLsizei count, const GLdouble* value) { - Uniformv_State<3>(location, count, value); + UniformvNarrowed_State<3>(location, count, value); } void ProgramUniform3d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2) { const GLdouble v[] = {v0, v1, v2}; - ProgramUniformv_State<3>(program, location, 1, v); + ProgramUniformvNarrowed_State<3>(program, location, 1, v); } void ProgramUniform3dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) { - ProgramUniformv_State<3>(program, location, count, value); + ProgramUniformvNarrowed_State<3>(program, location, count, value); } void Uniform4d(GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) { const GLdouble v[] = {v0, v1, v2, v3}; - Uniformv_State<4>(location, 1, v); + UniformvNarrowed_State<4>(location, 1, v); } void Uniform4dv(GLint location, GLsizei count, const GLdouble* value) { - Uniformv_State<4>(location, count, value); + UniformvNarrowed_State<4>(location, count, value); } void ProgramUniform4d(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3) { const GLdouble v[] = {v0, v1, v2, v3}; - ProgramUniformv_State<4>(program, location, 1, v); + ProgramUniformvNarrowed_State<4>(program, location, 1, v); } void ProgramUniform4dv(GLuint program, GLint location, GLsizei count, const GLdouble* value) { - ProgramUniformv_State<4>(program, location, count, value); + ProgramUniformvNarrowed_State<4>(program, location, count, value); } void UniformMatrix2dv(GLint location, GLsizei count, GLboolean transpose, const GLdouble* value) { if (location == -1) return; diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index bc17ff79..b6a520cb 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -61,6 +61,7 @@ add_executable(MobileGLIntegrationTest Scenarios/ClearThenReadPixelsScenario.cpp Scenarios/DepthStencilReadbackScenario.cpp Scenarios/SsboArrayLengthScenario.cpp + Scenarios/DoublePrecisionScenario.cpp Scenarios/UniformInitializerScenario.cpp Scenarios/SwizzleAccessRoutineScenario.cpp Scenarios/ProgramPipelineScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.cpp new file mode 100644 index 00000000..51c37a61 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/DoublePrecisionScenario.cpp @@ -0,0 +1,318 @@ +// 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 +#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_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; + std::string m_buildLog; + }; + + 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 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(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 the type it is actually stored as. + 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)); + } + + } // namespace +} // namespace MGITest diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index da40d644..515a9a2e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -326,15 +326,22 @@ namespace MobileGL::MG_State::GLState { : kInvalidUniformOffset; } Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); } - // Bytes a uniform actually occupies in the global UBO, which is not its GL type size: - // std140 pads each column of a float matrix out to a vec4, so a mat3 spans 48 bytes - // even though only 36 of them carry components. Anything reading or writing a whole - // uniform's storage - a bounds check, a copy between two programs' shadows - wants - // this rather than GetUniformSizesInBytes. + // Bytes a uniform actually occupies in the global UBO, which is not its GL type size, + // for two reasons. std140 pads each column of a matrix out to a vec4, so a mat3 spans + // 48 bytes even though only 36 of them carry components. And every 64-bit float in a + // shader is narrowed to 32 bits before the module reaches a backend + // (ShaderTranspiler::DemoteFloat64Pass) - the global UBO is laid out by reflecting that + // demoted module - so a `double` uniform occupies exactly what its float-typed twin + // would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything + // reading or writing a whole uniform's storage - a bounds check, a copy between two + // programs' shadows - wants this rather than GetUniformSizesInBytes. static SizeT UniformStorageSpanInBytes(const glslang::TType* type, SizeT tightSize) { - if (type != nullptr && type->isMatrix() && type->getBasicType() != glslang::EbtDouble) { + if (type != nullptr && type->isMatrix()) { return static_cast(type->getMatrixCols()) * 4 * sizeof(Float); } + if (type != nullptr && type->getBasicType() == glslang::EbtDouble) { + return tightSize / 2; + } return tightSize; } SizeT GetUniformStorageSpanInBytes(Uint location) const {