diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index d3c3c4a7..ad77b9ce 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -265,7 +265,7 @@ DECLARE_GL_FUNCTION_HEAD(void, ClearBufferfv, GLenum buffer, GLint drawbuffer, c DECLARE_GL_FUNCTION_HEAD(void, ClearBufferfi, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ClearBufferfi, buffer, drawbuffer, depth, stencil) DECLARE_GL_FUNCTION_HEAD(void, CopyBufferSubData, GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size) DECLARE_GL_FUNCTION_END_NO_RETURN(void, CopyBufferSubData, readTarget, writeTarget, readOffset, writeOffset, size) DECLARE_GL_FUNCTION_HEAD(void, GetUniformIndices, GLuint program, GLsizei uniformCount, const GLchar* const* uniformNames, GLuint* uniformIndices) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetUniformIndices, program, uniformCount, uniformNames, uniformIndices) -DECLARE_GL_FUNCTION_STUB_HEAD(void, GetActiveUniformsiv, GLuint program, GLsizei uniformCount, const GLuint* uniformIndices, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetActiveUniformsiv, program, uniformCount, uniformIndices, pname, params) +DECLARE_GL_FUNCTION_HEAD(void, GetActiveUniformsiv, GLuint program, GLsizei uniformCount, const GLuint* uniformIndices, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveUniformsiv, program, uniformCount, uniformIndices, pname, params) DECLARE_GL_FUNCTION_HEAD(GLuint, GetUniformBlockIndex, GLuint program, const GLchar* uniformBlockName) DECLARE_GL_FUNCTION_END(GLuint, GetUniformBlockIndex, program, uniformBlockName) DECLARE_GL_FUNCTION_HEAD(void, GetActiveUniformBlockiv, GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveUniformBlockiv, program, uniformBlockIndex, pname, params) DECLARE_GL_FUNCTION_HEAD(void, GetActiveUniformBlockName, GLuint program, GLuint uniformBlockIndex, GLsizei bufSize, GLsizei* length, GLchar* uniformBlockName) DECLARE_GL_FUNCTION_END_NO_RETURN(void, GetActiveUniformBlockName, program, uniformBlockIndex, bufSize, length, uniformBlockName) diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index 2191c177..ff2dc65a 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -424,6 +424,101 @@ namespace MobileGL::MG_Impl::GLImpl { } } + void GetActiveUniformsiv_State(GLuint program, GLsizei uniformCount, const GLuint* uniformIndices, GLenum pname, + GLint* params) { + if (uniformCount < 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "uniformCount " + std::to_string(uniformCount) + " is less than 0.")); + return; + } + + // Program-name resolution with the correct two-error split: a live shader name is + // GL_INVALID_OPERATION, a never-generated name is GL_INVALID_VALUE. glGetActiveUniformsiv has + // no "not linked" error, so unlike TryToGetLinkedProgramForInterfaceQuery there is no + // link-status check here; an unlinked program simply has zero active uniforms (handled below). + if (!MG_State::pGLContext->ValidateProgramName(program)) { + const ErrorCode error = MG_State::pGLContext->ValidateShaderName(program) ? ErrorCode::InvalidOperation + : ErrorCode::InvalidValue; + MG_State::pGLContext->RecordError( + error, MakeUnique("MG_Impl/GLImpl", __func__, + std::to_string(program) + " is not a program object.")); + return; + } + auto& programObject = MG_State::pGLContext->GetProgramObject(program); + if (!programObject) return; + + switch (pname) { + case GL_UNIFORM_TYPE: + case GL_UNIFORM_SIZE: + case GL_UNIFORM_NAME_LENGTH: + case GL_UNIFORM_BLOCK_INDEX: + case GL_UNIFORM_OFFSET: + case GL_UNIFORM_ARRAY_STRIDE: + case GL_UNIFORM_MATRIX_STRIDE: + case GL_UNIFORM_IS_ROW_MAJOR: + break; + default: + MG_State::pGLContext->RecordError( + ErrorCode::InvalidEnum, + MakeUnique("MG_Impl/GLImpl", __func__, + "pname " + std::to_string(pname) + " is not an accepted value.")); + return; + } + + if (uniformCount == 0) return; + if (uniformIndices == nullptr || params == nullptr) return; + + // Every index must be < the number of active uniforms, checked before any write so params is + // left untouched on error. GetUniformCount() is 0 for an unlinked program, which is also the + // spec-mandated GL_INVALID_VALUE path for querying an unlinked program (no separate error). + const Uint activeUniforms = programObject->GetUniformCount(); + for (GLsizei i = 0; i < uniformCount; ++i) { + if (uniformIndices[i] >= activeUniforms) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, + "uniformIndices[" + std::to_string(i) + + "] = " + std::to_string(uniformIndices[i]) + + " is greater than or equal to the number of active uniforms.")); + return; + } + } + + for (GLsizei i = 0; i < uniformCount; ++i) { + const Uint idx = uniformIndices[i]; + switch (pname) { + case GL_UNIFORM_TYPE: + params[i] = static_cast(programObject->GetActiveUniformType(idx)); + break; + case GL_UNIFORM_SIZE: + params[i] = programObject->GetActiveUniformArraySize(idx); + break; + case GL_UNIFORM_NAME_LENGTH: + params[i] = static_cast(programObject->GetActiveUniformName(idx).length() + 1); + break; + case GL_UNIFORM_BLOCK_INDEX: + params[i] = programObject->GetActiveUniformBlockIndex(idx); + break; + case GL_UNIFORM_OFFSET: + params[i] = programObject->GetActiveUniformOffset(idx); + break; + case GL_UNIFORM_ARRAY_STRIDE: + params[i] = programObject->GetActiveUniformArrayStride(idx); + break; + case GL_UNIFORM_MATRIX_STRIDE: + params[i] = programObject->GetActiveUniformMatrixStride(idx); + break; + case GL_UNIFORM_IS_ROW_MAJOR: + params[i] = programObject->GetActiveUniformIsRowMajor(idx); + break; + default: + break; + } + } + } + void GetAttachedShaders_State(GLuint program, GLsizei maxCount, GLsizei* count, GLuint* shaders) { if (maxCount < 0) { MG_State::pGLContext->RecordError( @@ -1503,6 +1598,11 @@ namespace MobileGL::MG_Impl::GLImpl { GetUniformIndices_State(program, uniformCount, uniformNames, uniformIndices); } + void GetActiveUniformsiv(GLuint program, GLsizei uniformCount, const GLuint* uniformIndices, GLenum pname, + GLint* params) { + GetActiveUniformsiv_State(program, uniformCount, uniformIndices, pname, params); + } + void GetAttachedShaders(GLuint program, GLsizei maxCount, GLsizei* count, GLuint* shaders) { GetAttachedShaders_State(program, maxCount, count, shaders); } diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h index 91b243aa..5d7794e3 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h @@ -26,6 +26,8 @@ namespace MobileGL::MG_Impl::GLImpl { GLchar* uniformName); void GetUniformIndices(GLuint program, GLsizei uniformCount, const GLchar* const* uniformNames, GLuint* uniformIndices); + void GetActiveUniformsiv(GLuint program, GLsizei uniformCount, const GLuint* uniformIndices, GLenum pname, + GLint* params); void GetAttachedShaders(GLuint program, GLsizei maxCount, GLsizei* count, GLuint* shaders); GLint GetAttribLocation(GLuint program, const GLchar* name); void GetProgramiv(GLuint program, GLenum pname, GLint* params); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index b53b6046..9126af96 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -75,9 +75,17 @@ namespace MobileGL::MG_State::GLState { return uniform.glDefineType; } + // Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array. + // glslang's TObjectReflection.size only carries the element count for a NON-block array; for + // a block array member it reports 1, so take the count from the TType, which is authoritative + // for both. GL 3.3 core uniforms are always sized. GLint GetActiveUniformArraySize(Uint index) const { - auto& uniform = m_program->getUniform(static_cast(index)); - return uniform.size; + const auto& uniform = m_program->getUniform(static_cast(index)); + const glslang::TType* type = uniform.getType(); + if (type != nullptr && type->isSizedArray()) { + return type->getOuterArraySize(); + } + return uniform.size < 1 ? 1 : uniform.size; } Int GetActiveUniformBlockIndex(Uint index) const { @@ -85,6 +93,63 @@ namespace MobileGL::MG_State::GLState { return uniform.index; } + // GL_UNIFORM_OFFSET: byte offset within the owning named block. glslang already reports -1 + // for a default-block uniform, which is exactly the spec value there. + GLint GetActiveUniformOffset(Uint index) const { + return m_program->getUniform(static_cast(index)).offset; + } + + // GL_UNIFORM_ARRAY_STRIDE: byte stride of an array member in a named block; 0 for a non-array + // block member; -1 for a default-block uniform. glslang yields arrayStride==0 for the + // default-block case, so gate on block membership to return the spec-mandated -1. + GLint GetActiveUniformArrayStride(Uint index) const { + const auto& uniform = m_program->getUniform(static_cast(index)); + return (uniform.index < 0) ? -1 : uniform.arrayStride; + } + + // GL_UNIFORM_IS_ROW_MAJOR: 1 only for a row-major matrix in a named block, else 0. The + // isMatrix() guard is required -- glslang stamps a block-level layout(row_major) onto + // non-matrix members too, so a float/vec in a row_major block would otherwise report 1. + // For the glslang build here a block-level layout(row_major) is also resolved onto each + // matrix member's own qualifier (verified by GetActiveUniformsivRowMajorBlock), so the member + // check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves + // an inheriting member's layoutMatrix == ElmNone. + GLint GetActiveUniformIsRowMajor(Uint index) const { + const auto& uniform = m_program->getUniform(static_cast(index)); + if (uniform.index < 0) return 0; + const glslang::TType* type = uniform.getType(); + if (type == nullptr || !type->isMatrix()) return 0; + glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix; + if (layoutMatrix == glslang::ElmNone) { + layoutMatrix = m_program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix; + } + return (layoutMatrix == glslang::ElmRowMajor) ? 1 : 0; + } + + // GL_UNIFORM_MATRIX_STRIDE: byte stride between columns (col-major) / rows (row-major) of a + // matrix in a named block; 0 for a non-matrix block member; -1 for a default-block uniform. + // glslang exposes no matrix stride, so it is derived from the std140 rule -- each column/row + // vector's base alignment rounded up to a vec4 (16 B). MobileGL's SPIR-V path lays every UBO + // out as std140 (packed/shared are coerced), so this matches the offsets glslang reports. For + // every GL 3.3 float matrix this evaluates to 16, independent of majorness. + GLint GetActiveUniformMatrixStride(Uint index) const { + const auto& uniform = m_program->getUniform(static_cast(index)); + if (uniform.index < 0) return -1; + const glslang::TType* type = uniform.getType(); + if (type == nullptr || !type->isMatrix()) return 0; + glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix; + if (layoutMatrix == glslang::ElmNone) { + layoutMatrix = m_program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix; + } + const bool rowMajor = (layoutMatrix == glslang::ElmRowMajor); + const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows(); + constexpr int scalarSize = 4; // GL 3.3 core uniform matrices are float + const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize + : (strideVectorComponents == 2) ? 2 * scalarSize + : 4 * scalarSize; + return (vectorAlignment + 15) & ~15; // std140 round-up to a vec4 + } + const glslang::TType* GetUniformTType(Uint location) const { auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]); return uniform.getType(); diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index 2090fd60..81ac07c6 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -1602,3 +1602,186 @@ TEST_F(ProgramTest, CompileShaderWithSamplerAsVarName) { spvcSession.Compile(&result); printf("decomp from fragSpirv:\n%s\n\n", result); } + +namespace { + // Links a VS+FS pair whose fragment shader carries a std140 uniform block (scalar + array + mat4) + // plus a default-block sampler, and returns the linked program. matrixLayout lets a test flip the + // block to row_major. + GLuint LinkUboReflectionProgram(const char* matrixLayout) { + char infoLog[1024] = ""; + const char* vsSrc = R"(#version 330 core +void main() { gl_Position = vec4(0.0); } +)"; + std::string fsSrc = std::string("#version 330 core\n") + + "layout(std140" + matrixLayout + ") uniform Block {\n" + + " float uScalar;\n" + + " vec4 uArray[3];\n" + + " mat4 uMatrix;\n" + + "};\n" + + "uniform sampler2D uTex;\n" + + "out vec4 fragColor;\n" + + "void main() {\n" + + " fragColor = texture(uTex, uArray[0].xy) * uScalar * uMatrix[0];\n" + + "}\n"; + const char* fsPtr = fsSrc.c_str(); + + GLuint vs = CreateShader(GL_VERTEX_SHADER); + ShaderSource(vs, 1, &vsSrc, nullptr); + CompileShader(vs); + GLint vsStatus = GL_FALSE; + GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus); + GetShaderInfoLog(vs, sizeof(infoLog), nullptr, infoLog); + EXPECT_EQ(vsStatus, GL_TRUE) << infoLog; + + GLuint fs = CreateShader(GL_FRAGMENT_SHADER); + ShaderSource(fs, 1, &fsPtr, nullptr); + CompileShader(fs); + GLint fsStatus = GL_FALSE; + GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus); + GetShaderInfoLog(fs, sizeof(infoLog), nullptr, infoLog); + EXPECT_EQ(fsStatus, GL_TRUE) << infoLog; + + GLuint program = CreateProgram(); + AttachShader(program, vs); + AttachShader(program, fs); + LinkProgram(program); + GLint linkStatus = GL_FALSE; + GetProgramiv(program, GL_LINK_STATUS, &linkStatus); + GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog); + EXPECT_EQ(linkStatus, GL_TRUE) << infoLog; + return program; + } + + GLuint UniformIndexByName(GLuint program, const char* name) { + GLuint index = GL_INVALID_INDEX; + GetUniformIndices(program, 1, &name, &index); + return index; + } + + GLint QueryUniformiv(GLuint program, GLuint uniformIndex, GLenum pname) { + GLint value = -12345; // sentinel that is not a legal answer for any queried pname + GetActiveUniformsiv(program, 1, &uniformIndex, pname, &value); + return value; + } +} // namespace + +TEST_F(ProgramTest, GetActiveUniformsivStd140Block) { + GLuint program = LinkUboReflectionProgram(/*matrixLayout=*/""); + + GLint activeUniforms = 0; + GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeUniforms); + EXPECT_EQ(activeUniforms, 4); + + const GLuint s = UniformIndexByName(program, "uScalar"); + const GLuint a = UniformIndexByName(program, "uArray"); + const GLuint m = UniformIndexByName(program, "uMatrix"); + const GLuint t = UniformIndexByName(program, "uTex"); + ASSERT_NE(s, GL_INVALID_INDEX); + ASSERT_NE(a, GL_INVALID_INDEX); + ASSERT_NE(m, GL_INVALID_INDEX); + ASSERT_NE(t, GL_INVALID_INDEX); + + // Types and sizes. + EXPECT_EQ(QueryUniformiv(program, s, GL_UNIFORM_TYPE), GL_FLOAT); + EXPECT_EQ(QueryUniformiv(program, a, GL_UNIFORM_TYPE), GL_FLOAT_VEC4); + EXPECT_EQ(QueryUniformiv(program, m, GL_UNIFORM_TYPE), GL_FLOAT_MAT4); + EXPECT_EQ(QueryUniformiv(program, t, GL_UNIFORM_TYPE), GL_SAMPLER_2D); + EXPECT_EQ(QueryUniformiv(program, s, GL_UNIFORM_SIZE), 1); + EXPECT_EQ(QueryUniformiv(program, a, GL_UNIFORM_SIZE), 3); + EXPECT_EQ(QueryUniformiv(program, m, GL_UNIFORM_SIZE), 1); + + // Block membership: -1 for the default-block sampler. + EXPECT_GE(QueryUniformiv(program, s, GL_UNIFORM_BLOCK_INDEX), 0); + EXPECT_EQ(QueryUniformiv(program, t, GL_UNIFORM_BLOCK_INDEX), -1); + + // std140 offsets. + EXPECT_EQ(QueryUniformiv(program, s, GL_UNIFORM_OFFSET), 0); + EXPECT_EQ(QueryUniformiv(program, a, GL_UNIFORM_OFFSET), 16); + EXPECT_EQ(QueryUniformiv(program, m, GL_UNIFORM_OFFSET), 64); + EXPECT_EQ(QueryUniformiv(program, t, GL_UNIFORM_OFFSET), -1); + + // ARRAY_STRIDE: 16 for the array, 0 for non-array block members, -1 for the default block. + EXPECT_EQ(QueryUniformiv(program, a, GL_UNIFORM_ARRAY_STRIDE), 16); + EXPECT_EQ(QueryUniformiv(program, s, GL_UNIFORM_ARRAY_STRIDE), 0); + EXPECT_EQ(QueryUniformiv(program, m, GL_UNIFORM_ARRAY_STRIDE), 0); + EXPECT_EQ(QueryUniformiv(program, t, GL_UNIFORM_ARRAY_STRIDE), -1); + + // MATRIX_STRIDE: 16 for the matrix, 0 for non-matrix block members, -1 for the default block. + EXPECT_EQ(QueryUniformiv(program, m, GL_UNIFORM_MATRIX_STRIDE), 16); + EXPECT_EQ(QueryUniformiv(program, s, GL_UNIFORM_MATRIX_STRIDE), 0); + EXPECT_EQ(QueryUniformiv(program, a, GL_UNIFORM_MATRIX_STRIDE), 0); + EXPECT_EQ(QueryUniformiv(program, t, GL_UNIFORM_MATRIX_STRIDE), -1); + + // Column-major block: nothing is row-major. + EXPECT_EQ(QueryUniformiv(program, m, GL_UNIFORM_IS_ROW_MAJOR), 0); + EXPECT_EQ(QueryUniformiv(program, s, GL_UNIFORM_IS_ROW_MAJOR), 0); + + // NAME_LENGTH includes the terminator and matches glGetActiveUniform's reported name. + char nameBuf[64] = ""; + GLsizei nameLen = 0; + GLint size = 0; + GLenum type = 0; + GetActiveUniform(program, m, sizeof(nameBuf), &nameLen, &size, &type, nameBuf); + EXPECT_EQ(QueryUniformiv(program, m, GL_UNIFORM_NAME_LENGTH), + static_cast(std::strlen(nameBuf) + 1)); + + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +// A block-level layout(row_major) with no per-member qualifier: only the matrix is row-major, and +// only via the block-inheritance fallback (member layoutMatrix == ElmNone). A naive per-member check +// returns 0 here. +TEST_F(ProgramTest, GetActiveUniformsivRowMajorBlock) { + GLuint program = LinkUboReflectionProgram(/*matrixLayout=*/", row_major"); + + const GLuint s = UniformIndexByName(program, "uScalar"); + const GLuint a = UniformIndexByName(program, "uArray"); + const GLuint m = UniformIndexByName(program, "uMatrix"); + ASSERT_NE(m, GL_INVALID_INDEX); + + EXPECT_EQ(QueryUniformiv(program, m, GL_UNIFORM_IS_ROW_MAJOR), 1); + EXPECT_EQ(QueryUniformiv(program, s, GL_UNIFORM_IS_ROW_MAJOR), 0); // non-matrix, isMatrix() guard + EXPECT_EQ(QueryUniformiv(program, a, GL_UNIFORM_IS_ROW_MAJOR), 0); + EXPECT_EQ(QueryUniformiv(program, m, GL_UNIFORM_MATRIX_STRIDE), 16); // unchanged by majorness + EXPECT_EQ(GetError(), GL_NO_ERROR); +} + +TEST_F(ProgramTest, GetActiveUniformsivErrors) { + GLuint program = LinkUboReflectionProgram(/*matrixLayout=*/""); + GLint activeUniforms = 0; + GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeUniforms); + ASSERT_GT(activeUniforms, 0); + + GLuint validIndex = 0; + GLint params[4] = {-999, -999, -999, -999}; + + // E1: negative count -> GL_INVALID_VALUE, params untouched. + GetActiveUniformsiv(program, -1, &validIndex, GL_UNIFORM_TYPE, params); + EXPECT_EQ(GetError(), GL_INVALID_VALUE); + EXPECT_EQ(params[0], -999); + + // E2: index == ACTIVE_UNIFORMS -> GL_INVALID_VALUE, params untouched. + GLuint outOfRange = static_cast(activeUniforms); + GetActiveUniformsiv(program, 1, &outOfRange, GL_UNIFORM_TYPE, params); + EXPECT_EQ(GetError(), GL_INVALID_VALUE); + EXPECT_EQ(params[0], -999); + + // E3: GL 4.2 token -> GL_INVALID_ENUM here. + GetActiveUniformsiv(program, 1, &validIndex, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX, params); + EXPECT_EQ(GetError(), GL_INVALID_ENUM); + EXPECT_EQ(params[0], -999); + + // E4a: a live shader name -> GL_INVALID_OPERATION. + GLuint shader = CreateShader(GL_VERTEX_SHADER); + GetActiveUniformsiv(shader, 1, &validIndex, GL_UNIFORM_TYPE, params); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION); + + // E4b: a never-generated name -> GL_INVALID_VALUE. + GetActiveUniformsiv(9999u, 1, &validIndex, GL_UNIFORM_TYPE, params); + EXPECT_EQ(GetError(), GL_INVALID_VALUE); + + // E6: zero count on a linked program is a valid no-op. + GetActiveUniformsiv(program, 0, &validIndex, GL_UNIFORM_TYPE, params); + EXPECT_EQ(GetError(), GL_NO_ERROR); + EXPECT_EQ(params[0], -999); +}