Files
MobileGL/MobileGL/MG_Test/Program/ProgramInterfaceTest.cpp
T

1622 lines
82 KiB
C++

// MobileGL - MobileGL/MG_Test/Program/ProgramInterfaceTest.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
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) answered
// entirely from the frontend glslang reflection - MG_Impl/GLImpl/Program/ProgramInterface.
//
// GPU-free on purpose: every expectation below is a property of the LINKED PROGRAM, not of
// any driver, which is the whole point of the layer. The shaders and the expected values
// are lifted from KHR-GL4x.program_interface_query so a failure here is the same failure
// the conformance suite would report, minutes earlier and without a GPU.
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_State/GLState/Core.h"
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
namespace {
class ProgramInterfaceTest : public ::testing::Test {
protected:
void SetUp() override { MobileGL::Initialize(); }
};
GLuint MakeProgram(const char* vs, const char* fs, const char* cs = nullptr) {
const GLuint p = CreateProgram();
const auto attach = [p](GLenum stage, const char* source) {
if (!source) return;
const GLuint sh = CreateShader(stage);
ShaderSource(sh, 1, &source, nullptr);
CompileShader(sh);
AttachShader(p, sh);
};
attach(GL_VERTEX_SHADER, vs);
attach(GL_FRAGMENT_SHADER, fs);
attach(GL_COMPUTE_SHADER, cs);
return p;
}
void ExpectLinked(GLuint program) {
GLint status = 0;
GetProgramiv(program, GL_LINK_STATUS, &status);
if (status == GL_TRUE) return;
char log[4096] = "";
GetProgramInfoLog(program, sizeof(log), nullptr, log);
FAIL() << "link failed: " << log;
}
GLint Interfaceiv(GLuint program, GLenum iface, GLenum pname) {
GLint value = -12345;
GetProgramInterfaceiv(program, iface, pname, &value);
return value;
}
std::string ResourceName(GLuint program, GLenum iface, GLuint index) {
GLchar buffer[1024] = {'\0'};
GLsizei length = 0;
GetProgramResourceName(program, iface, index, sizeof(buffer), &length, buffer);
EXPECT_GE(length, 0);
EXPECT_EQ(buffer[length], '\0') << "length must not count the terminator";
return std::string(buffer);
}
// Resolves by name and immediately checks that the index round-trips back to the
// expected spelling, which is how the CTS uses these two together.
void ExpectResource(GLuint program, GLenum iface, const char* queryName, const char* enumeratedName) {
const GLuint index = GetProgramResourceIndex(program, iface, queryName);
ASSERT_NE(index, GL_INVALID_INDEX) << "no resource named '" << queryName << "'";
EXPECT_EQ(ResourceName(program, iface, index), enumeratedName) << "for query '" << queryName << "'";
}
std::vector<GLint> Props(GLuint program, GLenum iface, GLuint index, const std::vector<GLenum>& props) {
std::vector<GLint> params(256, -12345);
GLsizei length = 0;
GetProgramResourceiv(program, iface, index, static_cast<GLsizei>(props.size()), props.data(),
static_cast<GLsizei>(params.size()), &length, params.data());
params.resize(length < 0 ? 0 : static_cast<size_t>(length));
return params;
}
std::vector<GLint> PropsOf(GLuint program, GLenum iface, const char* name, const std::vector<GLenum>& props) {
const GLuint index = GetProgramResourceIndex(program, iface, name);
EXPECT_NE(index, GL_INVALID_INDEX) << "no resource named '" << name << "'";
if (index == GL_INVALID_INDEX) return {};
return Props(program, iface, index, props);
}
GLenum TakeError() { return GetError(); }
void ClearErrors() {
for (int i = 0; i < 32 && TakeError() != GL_NO_ERROR; ++i) {
}
}
const char* kSimpleVs = R"(#version 430
in vec4 position;
void main(void) { gl_Position = position; }
)";
const char* kSimpleFs = R"(#version 430
out vec4 color;
void main() { color = vec4(0, 1, 0, 1); }
)";
// ---------------------------------------------------------------- simple-shaders ----
TEST_F(ProgramInterfaceTest, SimpleShaders) {
const GLuint p = MakeProgram(kSimpleVs, kSimpleFs);
BindAttribLocation(p, 0, "position");
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 6);
EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_OUTPUT, "color"), 0u);
EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_INPUT, "position"), 0u);
EXPECT_EQ(ResourceName(p, GL_PROGRAM_OUTPUT, 0), "color");
EXPECT_EQ(ResourceName(p, GL_PROGRAM_INPUT, 0), "position");
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "position"), 0);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "color"), 0);
EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, "color"), 0);
const std::vector<GLenum> inProps = {GL_NAME_LENGTH,
GL_TYPE,
GL_ARRAY_SIZE,
GL_REFERENCED_BY_COMPUTE_SHADER,
GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_REFERENCED_BY_GEOMETRY_SHADER,
GL_REFERENCED_BY_TESS_CONTROL_SHADER,
GL_REFERENCED_BY_TESS_EVALUATION_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER,
GL_LOCATION,
GL_IS_PER_PATCH};
EXPECT_EQ(Props(p, GL_PROGRAM_INPUT, 0, inProps),
(std::vector<GLint>{9, GL_FLOAT_VEC4, 1, 0, 0, 0, 0, 0, 1, 0, 0}));
std::vector<GLenum> outProps = inProps;
outProps.push_back(GL_LOCATION_INDEX);
EXPECT_EQ(Props(p, GL_PROGRAM_OUTPUT, 0, outProps),
(std::vector<GLint>{6, GL_FLOAT_VEC4, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------------------- input-types ----
TEST_F(ProgramInterfaceTest, InputTypesNamesLocationsAndProps) {
const char* vs = R"(#version 430
in mat4 a;
in ivec4 b;
in float c[2];
in mat2x3 d[2];
in uvec2 e;
in uint f;
in vec3 g[2];
in int h;
void main(void)
{
vec4 pos;
pos.w = h + g[0].x + g[1].y + d[1][1].y;
pos.y = b.x * c[0] + c[1] + d[0][0].x;
pos.x = a[0].x + a[1].y + a[2].z + a[3].w;
pos.z = d[0][1].z + e.x * f + d[1][0].z;
gl_Position = pos;
}
)";
const GLuint p = MakeProgram(vs, kSimpleFs);
BindAttribLocation(p, 0, "a");
BindAttribLocation(p, 4, "b");
BindAttribLocation(p, 5, "c");
BindAttribLocation(p, 7, "d");
BindAttribLocation(p, 11, "e");
BindAttribLocation(p, 12, "f");
BindAttribLocation(p, 13, "g");
BindAttribLocation(p, 15, "h");
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 8);
// "c[0]" and friends: an array input is enumerated with the [0] subscript, which is
// what makes MAX_NAME_LENGTH 5 rather than 2.
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 5);
ExpectResource(p, GL_PROGRAM_INPUT, "a", "a");
ExpectResource(p, GL_PROGRAM_INPUT, "c[0]", "c[0]");
ExpectResource(p, GL_PROGRAM_INPUT, "c", "c[0]");
ExpectResource(p, GL_PROGRAM_INPUT, "d", "d[0]");
ExpectResource(p, GL_PROGRAM_INPUT, "g", "g[0]");
ExpectResource(p, GL_PROGRAM_INPUT, "h", "h");
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "a"), 0);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "b"), 4);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c[0]"), 5);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c"), 5);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c[1]"), 6);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "d[0]"), 7);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "g[1]"), 14);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "h"), 15);
// Out of range, and a subscript that is not a strict decimal.
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c[2]"), -1);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "c[01]"), -1);
const std::vector<GLenum> props = {GL_NAME_LENGTH,
GL_TYPE,
GL_ARRAY_SIZE,
GL_REFERENCED_BY_COMPUTE_SHADER,
GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER,
GL_LOCATION,
GL_IS_PER_PATCH};
EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "a", props),
(std::vector<GLint>{2, GL_FLOAT_MAT4, 1, 0, 0, 1, 0, 0}));
EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "c[0]", props),
(std::vector<GLint>{5, GL_FLOAT, 2, 0, 0, 1, 5, 0}));
EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "d", props),
(std::vector<GLint>{5, GL_FLOAT_MAT2x3, 2, 0, 0, 1, 7, 0}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------------------ output-types ----
TEST_F(ProgramInterfaceTest, OutputTypesAndLocationIndex) {
const char* fs = R"(#version 430
out vec3 a[2];
out uint b;
out float c[2];
out int d[2];
out vec2 e;
void main() {
c[1] = -0.6; d[0] = 0; b = 12u; c[0] = 1.1; e = vec2(0, 1); d[1] = -19;
a[1] = vec3(0, 1, 0); a[0] = vec3(0, 1, 0);
}
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
BindAttribLocation(p, 0, "position");
BindFragDataLocation(p, 0, "a");
BindFragDataLocation(p, 2, "b");
BindFragDataLocation(p, 3, "c");
BindFragDataLocation(p, 5, "d");
BindFragDataLocation(p, 7, "e");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 5);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 5);
ExpectResource(p, GL_PROGRAM_OUTPUT, "a", "a[0]");
ExpectResource(p, GL_PROGRAM_OUTPUT, "c[0]", "c[0]");
ExpectResource(p, GL_PROGRAM_OUTPUT, "e", "e");
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "a[0]"), 0);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "a"), 0);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "a[1]"), 1);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "b"), 2);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "c[1]"), 4);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "d[1]"), 6);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "e"), 7);
for (const char* name : {"a[0]", "a", "b", "c[0]", "c", "d[0]", "d", "e"}) {
EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, name), 0) << name;
}
const std::vector<GLenum> props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_LOCATION, GL_IS_PER_PATCH, GL_LOCATION_INDEX};
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "a", props),
(std::vector<GLint>{5, GL_FLOAT_VEC3, 2, 1, 0, 0, 0}));
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "d", props), (std::vector<GLint>{5, GL_INT, 2, 1, 5, 0, 0}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// --------------------------------------------------------------- output-built-in ----
TEST_F(ProgramInterfaceTest, OutputBuiltInsHaveNoLocation) {
const char* fs = R"(#version 430
void main(void) { gl_FragDepth = 0.1; gl_SampleMask[0] = 1; }
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 2);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 17);
ExpectResource(p, GL_PROGRAM_OUTPUT, "gl_FragDepth", "gl_FragDepth");
ExpectResource(p, GL_PROGRAM_OUTPUT, "gl_SampleMask[0]", "gl_SampleMask[0]");
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "gl_FragDepth"), -1);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_OUTPUT, "gl_SampleMask"), -1);
EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, "gl_FragDepth"), -1);
EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, "gl_SampleMask[0]"), -1);
const std::vector<GLenum> props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_LOCATION, GL_LOCATION_INDEX};
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_FragDepth", props),
(std::vector<GLint>{13, GL_FLOAT, 1, 1, -1, -1}));
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_SampleMask[0]", props),
(std::vector<GLint>{17, GL_INT, 1, 1, -1, -1}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------- input-built-in ----
TEST_F(ProgramInterfaceTest, InputBuiltInsUseGlSpellings) {
const char* vs = R"(#version 430
void main(void) { gl_Position = (gl_VertexID + gl_InstanceID) * vec4(0.1); }
)";
const GLuint p = MakeProgram(vs, kSimpleFs);
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 2);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 14);
// The Vulkan-semantics parse calls these gl_VertexIndex / gl_InstanceIndex.
ExpectResource(p, GL_PROGRAM_INPUT, "gl_VertexID", "gl_VertexID");
ExpectResource(p, GL_PROGRAM_INPUT, "gl_InstanceID", "gl_InstanceID");
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "gl_VertexID"), -1);
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "gl_InstanceID"), -1);
const std::vector<GLenum> props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_REFERENCED_BY_VERTEX_SHADER,
GL_LOCATION};
EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "gl_VertexID", props), (std::vector<GLint>{12, GL_INT, 1, 1, -1}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------------------ uniform-simple --
TEST_F(ProgramInterfaceTest, UniformSimple) {
const char* vs = R"(#version 430
in vec4 position;
uniform vec4 repos;
void main(void) { gl_Position = position + repos; }
)";
const char* fs = R"(#version 430
uniform vec4 recolor;
out vec4 color;
void main() { color = vec4(0, 1, 0, 1) + recolor; }
)";
const GLuint p = MakeProgram(vs, fs);
BindAttribLocation(p, 0, "position");
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
GLint activeUniforms = 0;
GetProgramiv(p, GL_ACTIVE_UNIFORMS, &activeUniforms);
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_ACTIVE_RESOURCES), activeUniforms);
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_MAX_NAME_LENGTH), 8);
ExpectResource(p, GL_UNIFORM, "repos", "repos");
ExpectResource(p, GL_UNIFORM, "recolor", "recolor");
// The sharpest single symptom of the old backend-forwarding design: these two had to
// agree and did not, because the ESSL program keeps "repos" inside MGL_GLOBAL_UBO.
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "repos"), GetUniformLocation(p, "repos"));
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "recolor"), GetUniformLocation(p, "recolor"));
const std::vector<GLenum> props = {GL_NAME_LENGTH,
GL_TYPE,
GL_ARRAY_SIZE,
GL_OFFSET,
GL_BLOCK_INDEX,
GL_ARRAY_STRIDE,
GL_MATRIX_STRIDE,
GL_IS_ROW_MAJOR,
GL_ATOMIC_COUNTER_BUFFER_INDEX,
GL_REFERENCED_BY_COMPUTE_SHADER,
GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER,
GL_LOCATION};
EXPECT_EQ(PropsOf(p, GL_UNIFORM, "repos", props),
(std::vector<GLint>{6, GL_FLOAT_VEC4, 1, -1, -1, -1, -1, 0, -1, 0, 0, 1,
GetUniformLocation(p, "repos")}));
EXPECT_EQ(PropsOf(p, GL_UNIFORM, "recolor", props),
(std::vector<GLint>{8, GL_FLOAT_VEC4, 1, -1, -1, -1, -1, 0, -1, 0, 1, 0,
GetUniformLocation(p, "recolor")}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------------------- array-names ----
TEST_F(ProgramInterfaceTest, ArrayNameSubscriptsAreStrictDecimals) {
const char* vs = R"(#version 430
in vec4 position;
uniform vec4 a[2];
void main(void) { gl_Position = position + a[0] + a[1]; }
)";
const GLuint p = MakeProgram(vs, kSimpleFs);
BindAttribLocation(p, 0, "position");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "a"), GetUniformLocation(p, "a"));
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "a[0]"), GetUniformLocation(p, "a"));
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "a[1]"), GetUniformLocation(p, "a[1]"));
for (const char* bad : {"a[2]", "a[0 + 0]", "a[0+0]", "a[ 0]", "a[0 ]", "a[\n0]", "a[\t0]", "a[01]", "a[00]"}) {
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, bad), -1) << "for '" << bad << "'";
}
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// -------------------------------------------------------------- arrays-of-arrays ----
TEST_F(ProgramInterfaceTest, ArraysOfArrays) {
const char* vs = R"(#version 430
in vec4 position;
uniform vec4 a[3][4][5];
void main(void) {
gl_Position = position;
for (int i = 0; i < 5; ++i) gl_Position += a[2][1][i];
}
)";
const GLuint p = MakeProgram(vs, kSimpleFs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_MAX_NAME_LENGTH), 11);
ExpectResource(p, GL_UNIFORM, "a[2][1]", "a[2][1][0]");
EXPECT_EQ(GetProgramResourceIndex(p, GL_UNIFORM, "a[2][1][0]"),
GetProgramResourceIndex(p, GL_UNIFORM, "a[2][1]"));
const std::vector<GLenum> props = {GL_NAME_LENGTH,
GL_TYPE,
GL_ARRAY_SIZE,
GL_OFFSET,
GL_BLOCK_INDEX,
GL_ARRAY_STRIDE,
GL_MATRIX_STRIDE,
GL_IS_ROW_MAJOR,
GL_ATOMIC_COUNTER_BUFFER_INDEX,
GL_REFERENCED_BY_VERTEX_SHADER,
GL_LOCATION};
EXPECT_EQ(PropsOf(p, GL_UNIFORM, "a[2][1]", props),
(std::vector<GLint>{11, GL_FLOAT_VEC4, 5, -1, -1, -1, -1, 0, -1, 1,
GetUniformLocation(p, "a[2][1]")}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------------- uniform-block-types --
TEST_F(ProgramInterfaceTest, UniformBlocks) {
const char* vs = R"(#version 430
in vec4 position;
uniform SimpleBlock { mat3x2 a; mat4 b; vec4 c; };
uniform NotSoSimpleBlockk { ivec2 a[4]; mat3 b[2]; mat2 c; } d;
void main(void) {
float tmp = a[0][1] * b[1][2] * c.x;
tmp = tmp + d.a[2].y + d.b[0][1][1] + d.c[1][1];
gl_Position = position * tmp;
}
)";
const char* fs = R"(#version 430
struct U { bool a[3]; vec4 b; mat3 c; float d[2]; };
struct UU { U a; U b[2]; uvec2 c; };
uniform TrickyBlock { UU a[3]; mat4 b; uint c; } e[2];
out vec4 color;
void main() { color = vec4(0, 1, 0, 1) * e[0].a[2].b[0].d[1]; }
)";
const GLuint p = MakeProgram(vs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES), 4);
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_MAX_NAME_LENGTH), 18);
ExpectResource(p, GL_UNIFORM_BLOCK, "SimpleBlock", "SimpleBlock");
ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock", "TrickyBlock[0]");
ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock[1]", "TrickyBlock[1]");
ExpectResource(p, GL_UNIFORM, "NotSoSimpleBlockk.a[0]", "NotSoSimpleBlockk.a[0]");
ExpectResource(p, GL_UNIFORM, "TrickyBlock.a[2].b[0].d", "TrickyBlock.a[2].b[0].d[0]");
const GLuint simple = GetProgramResourceIndex(p, GL_UNIFORM_BLOCK, "SimpleBlock");
const GLuint tricky = GetProgramResourceIndex(p, GL_UNIFORM_BLOCK, "TrickyBlock");
// The index the interface hands out must be usable with glUniformBlockBinding.
UniformBlockBinding(p, simple, 0);
UniformBlockBinding(p, tricky, 3);
GLint dataSize = 0;
GetActiveUniformBlockiv(p, simple, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
EXPECT_EQ(Props(p, GL_UNIFORM_BLOCK, simple,
{GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES}),
(std::vector<GLint>{12, 0, 0, 1, dataSize, 3}));
EXPECT_EQ(Props(p, GL_UNIFORM_BLOCK, tricky,
{GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER}),
(std::vector<GLint>{15, 3, 1, 0}));
// A block member reports its block, no location, and no atomic-counter buffer.
EXPECT_EQ(PropsOf(p, GL_UNIFORM, "a",
{GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_ARRAY_STRIDE, GL_IS_ROW_MAJOR,
GL_ATOMIC_COUNTER_BUFFER_INDEX, GL_REFERENCED_BY_VERTEX_SHADER, GL_LOCATION}),
(std::vector<GLint>{2, GL_FLOAT_MAT3x2, 1, static_cast<GLint>(simple), 0, 0, -1, 1, -1}));
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "a"), -1);
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "b"), -1);
// GL_ACTIVE_VARIABLES lists exactly the three members, in GL_UNIFORM index space.
const std::vector<GLint> activeVariables = Props(p, GL_UNIFORM_BLOCK, simple, {GL_ACTIVE_VARIABLES});
ASSERT_EQ(activeVariables.size(), 3u);
for (const GLint variable : activeVariables) {
EXPECT_EQ(Props(p, GL_UNIFORM, static_cast<GLuint>(variable), {GL_BLOCK_INDEX}),
(std::vector<GLint>{static_cast<GLint>(simple)}));
}
EXPECT_GE(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 3);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ----------------------------------------------------------------- uniform-block-array
TEST_F(ProgramInterfaceTest, UniformBlockArrayMemberReportsItsBlock) {
const char* fs = R"(#version 430
uniform TestBlock { mediump vec4 color; } blockInstance[4];
out mediump vec4 color;
void main() { color = blockInstance[2].color + blockInstance[3].color; }
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
const GLuint block = GetProgramResourceIndex(p, GL_UNIFORM_BLOCK, "TestBlock");
ASSERT_NE(block, GL_INVALID_INDEX);
EXPECT_EQ(PropsOf(p, GL_UNIFORM, "TestBlock.color", {GL_BLOCK_INDEX}),
(std::vector<GLint>{static_cast<GLint>(block)}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------- ssb-types ----
TEST_F(ProgramInterfaceTest, ShaderStorageBlocksAndBufferVariables) {
const char* fs = R"(#version 430
struct U { bool a[3]; mediump vec4 b; mediump mat3 c; mediump float d[2]; };
struct UU { U a; U b[2]; uvec2 c; };
layout(binding=4) buffer TrickyBuffer { UU a[3]; mediump mat4 b; uint c; } e[2];
layout(binding = 0) buffer SimpleBuffer { mediump mat3x2 a; mediump mat4 b; mediump vec4 c; };
layout(binding = 1) buffer NotSoSimpleBuffer { ivec2 a[4]; mediump mat3 b[2]; mediump mat2 c; } d;
out mediump vec4 color;
void main() {
mediump float tmp = e[0].a[0].b[0].d[0] * float(e[1].c);
mediump float tmp2 = a[0][0] * b[0][0] * c.x;
tmp2 = tmp2 + float(d.a[0].y) + d.b[0][0][0] + d.c[0][0];
color = vec4(0, 1, 0, 1) * tmp * tmp2;
}
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 4);
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NAME_LENGTH), 18);
EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_MAX_NAME_LENGTH), 28);
EXPECT_GE(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_ACTIVE_RESOURCES), 7);
EXPECT_GE(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 3);
ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "SimpleBuffer", "SimpleBuffer");
ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "TrickyBuffer", "TrickyBuffer[0]");
ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "TrickyBuffer[1]", "TrickyBuffer[1]");
// A member of a block with no instance name keeps its bare name.
ExpectResource(p, GL_BUFFER_VARIABLE, "a", "a");
ExpectResource(p, GL_BUFFER_VARIABLE, "NotSoSimpleBuffer.a[0]", "NotSoSimpleBuffer.a[0]");
ExpectResource(p, GL_BUFFER_VARIABLE, "TrickyBuffer.a[0].b[0].d", "TrickyBuffer.a[0].b[0].d[0]");
const GLuint simple = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "SimpleBuffer");
const GLuint tricky = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "TrickyBuffer");
const GLuint tricky1 = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "TrickyBuffer[1]");
EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, simple,
{GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_NUM_ACTIVE_VARIABLES, GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER}),
(std::vector<GLint>{13, 0, 3, 1, 0}));
// An arrayed block gives element k the binding base + k.
EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, tricky, {GL_NAME_LENGTH, GL_BUFFER_BINDING}),
(std::vector<GLint>{16, 4}));
EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, tricky1, {GL_NAME_LENGTH, GL_BUFFER_BINDING}),
(std::vector<GLint>{16, 5}));
EXPECT_EQ(PropsOf(p, GL_BUFFER_VARIABLE, "a",
{GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_ARRAY_STRIDE, GL_IS_ROW_MAJOR,
GL_REFERENCED_BY_FRAGMENT_SHADER, GL_TOP_LEVEL_ARRAY_SIZE, GL_TOP_LEVEL_ARRAY_STRIDE}),
(std::vector<GLint>{2, GL_FLOAT_MAT3x2, 1, static_cast<GLint>(simple), 0, 0, 1, 1, 0}));
EXPECT_EQ(PropsOf(p, GL_BUFFER_VARIABLE, "TrickyBuffer.a[0].b[0].d",
{GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_MATRIX_STRIDE, GL_IS_ROW_MAJOR,
GL_REFERENCED_BY_FRAGMENT_SHADER, GL_TOP_LEVEL_ARRAY_SIZE}),
(std::vector<GLint>{28, GL_FLOAT, 2, static_cast<GLint>(tricky), 0, 0, 1, 3}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// --------------------------------------------------------------- top-level-array ----
TEST_F(ProgramInterfaceTest, TopLevelArray) {
const char* fs = R"(#version 430
buffer Block { vec4 a[5][4][3]; };
out vec4 color;
void main() { color = vec4(0, 1, 0, 1) + a[0][0][0]; }
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_MAX_NAME_LENGTH), 11);
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NAME_LENGTH), 6);
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 1);
ExpectResource(p, GL_BUFFER_VARIABLE, "a[0][0]", "a[0][0][0]");
ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "Block", "Block");
const GLuint block = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "Block");
EXPECT_EQ(PropsOf(p, GL_BUFFER_VARIABLE, "a[0][0]",
{GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_IS_ROW_MAJOR,
GL_REFERENCED_BY_FRAGMENT_SHADER, GL_TOP_LEVEL_ARRAY_SIZE}),
(std::vector<GLint>{11, GL_FLOAT_VEC4, 3, static_cast<GLint>(block), 0, 1, 5}));
const std::vector<GLint> stride = PropsOf(p, GL_BUFFER_VARIABLE, "a[0][0]", {GL_TOP_LEVEL_ARRAY_STRIDE});
ASSERT_EQ(stride.size(), 1u);
EXPECT_GT(stride[0], 0);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------- compute-shader ----
TEST_F(ProgramInterfaceTest, ComputeRuntimeSizedBufferVariable) {
const char* cs = R"(#version 430 core
layout(local_size_x = 1, local_size_y = 1) in;
layout(std430) buffer Output { vec4 data[]; } g_out;
void main() {
g_out.data[0] = vec4(1.0, 2.0, 3.0, 4.0);
g_out.data[100] = vec4(1.0, 2.0, 3.0, 4.0);
}
)";
const GLuint p = MakeProgram(nullptr, nullptr, cs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_MAX_NAME_LENGTH), 15);
EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NAME_LENGTH), 7);
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 1);
ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "Output", "Output");
ExpectResource(p, GL_BUFFER_VARIABLE, "Output.data", "Output.data[0]");
const GLuint block = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "Output");
const GLuint variable = GetProgramResourceIndex(p, GL_BUFFER_VARIABLE, "Output.data");
EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, block,
{GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_NUM_ACTIVE_VARIABLES, GL_REFERENCED_BY_COMPUTE_SHADER,
GL_REFERENCED_BY_FRAGMENT_SHADER, GL_REFERENCED_BY_VERTEX_SHADER, GL_ACTIVE_VARIABLES}),
(std::vector<GLint>{7, 0, 1, 1, 0, 0, static_cast<GLint>(variable)}));
// A runtime-sized array reports GL_ARRAY_SIZE 0 and a top-level array size of 1.
EXPECT_EQ(Props(p, GL_BUFFER_VARIABLE, variable,
{GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_IS_ROW_MAJOR,
GL_REFERENCED_BY_COMPUTE_SHADER, GL_TOP_LEVEL_ARRAY_SIZE}),
(std::vector<GLint>{15, GL_FLOAT_VEC4, 0, static_cast<GLint>(block), 0, 1, 1}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// --------------------------------------------------------------- atomic-counters ----
TEST_F(ProgramInterfaceTest, AtomicCounterBuffers) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 1, offset = 0) uniform atomic_uint a;
layout (binding = 2, offset = 0) uniform atomic_uint b;
layout (binding = 2, offset = 4) uniform atomic_uint c;
layout (binding = 5, offset = 0) uniform atomic_uint d[3];
layout (binding = 5, offset = 12) uniform atomic_uint e;
void main() {
uint x = atomicCounterIncrement(d[0]) + atomicCounterIncrement(a);
uint y = atomicCounterIncrement(d[1]) + atomicCounterIncrement(b);
uint z = atomicCounterIncrement(d[2]) + atomicCounterIncrement(c);
uint w = atomicCounterIncrement(e);
color = vec4(float(x), float(y), float(z), float(w));
}
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES), 3);
EXPECT_EQ(Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_MAX_NUM_ACTIVE_VARIABLES), 2);
ExpectResource(p, GL_UNIFORM, "a", "a");
ExpectResource(p, GL_UNIFORM, "d", "d[0]");
for (const char* name : {"a", "b", "c", "d", "e", "d[0]", "d[1]", "d[2]"}) {
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, name), -1) << name;
}
const auto bufferOf = [p](const char* uniform) {
const std::vector<GLint> value = PropsOf(p, GL_UNIFORM, uniform, {GL_ATOMIC_COUNTER_BUFFER_INDEX});
EXPECT_EQ(value.size(), 1u);
return value.empty() ? -1 : value[0];
};
const GLint bufferA = bufferOf("a");
const GLint bufferB = bufferOf("b");
const GLint bufferD = bufferOf("d");
ASSERT_GE(bufferA, 0);
EXPECT_EQ(bufferB, bufferOf("c"));
EXPECT_EQ(bufferD, bufferOf("e"));
EXPECT_NE(bufferA, bufferB);
EXPECT_EQ(Props(p, GL_ATOMIC_COUNTER_BUFFER, static_cast<GLuint>(bufferA),
{GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES, GL_ACTIVE_VARIABLES}),
(std::vector<GLint>{1, 4, 1, static_cast<GLint>(GetProgramResourceIndex(p, GL_UNIFORM, "a"))}));
EXPECT_EQ(Props(p, GL_ATOMIC_COUNTER_BUFFER, static_cast<GLuint>(bufferB),
{GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES}),
(std::vector<GLint>{2, 8, 2}));
EXPECT_EQ(Props(p, GL_ATOMIC_COUNTER_BUFFER, static_cast<GLuint>(bufferD),
{GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES}),
(std::vector<GLint>{5, 16, 2}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
// The interface has no resource names at all.
EXPECT_EQ(GetProgramResourceIndex(p, GL_ATOMIC_COUNTER_BUFFER, "a"), GL_INVALID_INDEX);
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
}
// The CLASSIC query surface has to agree with the interface query above. MobileGL lowers
// every atomic_uint onto a synthesized gl_AtomicCounterBlock_N, and glGetActiveUniform /
// glGetActiveUniformsiv used to report that lowering: GL_UNSIGNED_INT instead of
// GL_UNSIGNED_INT_ATOMIC_COUNTER, the synthesized block's index instead of the -1 a
// default-block uniform owes, and GL_INVALID_ENUM for
// GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX - the last of which is what made
// KHR-GL43.shader_atomic_counters.basic-program-query a forced FAIL.
TEST_F(ProgramInterfaceTest, AtomicCounterClassicUniformQueries) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 0, offset = 0) uniform atomic_uint ac_counter0;
layout (binding = 1, offset = 0) uniform atomic_uint ac_counter1;
uniform float plain;
void main() {
color = vec4(float(atomicCounterIncrement(ac_counter0) + atomicCounterIncrement(ac_counter1)) + plain);
}
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
const auto indexOf = [p](const char* name) {
const GLchar* names[1] = {name};
GLuint index = GL_INVALID_INDEX;
GetUniformIndices(p, 1, names, &index);
return index;
};
const auto uniformiv = [p](GLuint index, GLenum pname) {
GLint value = -12345;
const GLuint indices[1] = {index};
GetActiveUniformsiv(p, 1, indices, pname, &value);
return value;
};
const GLuint counter0 = indexOf("ac_counter0");
const GLuint counter1 = indexOf("ac_counter1");
const GLuint plain = indexOf("plain");
ASSERT_NE(counter0, GL_INVALID_INDEX);
ASSERT_NE(counter1, GL_INVALID_INDEX);
ASSERT_NE(plain, GL_INVALID_INDEX);
// (a) glGetActiveUniform and glGetActiveUniformsiv(GL_UNIFORM_TYPE) both report the
// GL-level type.
GLint size = 0;
GLenum type = 0;
GLchar nameBuffer[64] = {'\0'};
GetActiveUniform(p, counter0, sizeof(nameBuffer), nullptr, &size, &type, nameBuffer);
EXPECT_EQ(type, static_cast<GLenum>(GL_UNSIGNED_INT_ATOMIC_COUNTER));
EXPECT_EQ(std::string(nameBuffer), "ac_counter0");
EXPECT_EQ(uniformiv(counter0, GL_UNIFORM_TYPE), GL_UNSIGNED_INT_ATOMIC_COUNTER);
EXPECT_EQ(uniformiv(counter1, GL_UNIFORM_TYPE), GL_UNSIGNED_INT_ATOMIC_COUNTER);
EXPECT_EQ(uniformiv(plain, GL_UNIFORM_TYPE), GL_FLOAT);
// (b) an atomic counter is a DEFAULT-BLOCK uniform, whatever it was lowered onto.
EXPECT_EQ(uniformiv(counter0, GL_UNIFORM_BLOCK_INDEX), -1);
EXPECT_EQ(uniformiv(counter1, GL_UNIFORM_BLOCK_INDEX), -1);
EXPECT_EQ(uniformiv(plain, GL_UNIFORM_BLOCK_INDEX), -1);
// (c) the pname is accepted, answers with the buffer's index, and reports -1 for a
// uniform that is not a counter. Two bindings mean two distinct buffers.
const GLint buffer0 = uniformiv(counter0, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX);
const GLint buffer1 = uniformiv(counter1, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX);
EXPECT_GE(buffer0, 0);
EXPECT_GE(buffer1, 0);
EXPECT_NE(buffer0, buffer1);
EXPECT_LT(buffer0, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
EXPECT_LT(buffer1, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
EXPECT_EQ(uniformiv(plain, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX), -1);
// No leftover error: the CTS harness fails the subcase on one.
EXPECT_EQ(TakeError(), GL_NO_ERROR);
// The classic surface and the interface query name the same buffer.
const std::vector<GLint> interfaceBuffer =
PropsOf(p, GL_UNIFORM, "ac_counter0", {GL_ATOMIC_COUNTER_BUFFER_INDEX});
ASSERT_EQ(interfaceBuffer.size(), 1u);
EXPECT_EQ(interfaceBuffer[0], buffer0);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// Two counters that share a binding AND an offset must fail to link. glslang's own check
// lives in fixOffset(), which the Vulkan-relaxed parse never reaches - it folds the
// atomic_uint into a storage block and returns from declareVariable() first - so the pair
// used to link cleanly and then increment the same four bytes.
TEST_F(ProgramInterfaceTest, OverlappingAtomicCounterOffsetsFailToLink) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 0, offset = 0) uniform atomic_uint a;
layout (binding = 0, offset = 0) uniform atomic_uint b;
void main() { color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b))); }
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
GLint status = -1;
GetProgramiv(p, GL_LINK_STATUS, &status);
EXPECT_EQ(status, GL_FALSE);
char log[4096] = "";
GetProgramInfoLog(p, sizeof(log), nullptr, log);
EXPECT_NE(std::string(log).find("overlap"), std::string::npos) << "info log was: " << log;
ClearErrors();
// Distinct offsets at one binding, and the same offset at two different bindings, are
// both legal and must still link - a check keyed any wider would reject them.
const char* legalFs = R"(#version 430
out vec4 color;
layout (binding = 0, offset = 0) uniform atomic_uint a;
layout (binding = 0, offset = 4) uniform atomic_uint b;
layout (binding = 1, offset = 0) uniform atomic_uint c;
void main() {
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
}
)";
const GLuint legal = MakeProgram(kSimpleVs, legalFs);
LinkProgram(legal);
ExpectLinked(legal);
ClearErrors();
}
// GL 4.6 core 7.6 fails the link when a stage's active image uniforms exceed
// GL_MAX_*_IMAGE_UNIFORMS, or when their sum exceeds GL_MAX_COMBINED_IMAGE_UNIFORMS. Nothing
// counted them - glslang keeps those numbers only so gl_Max*ImageUniforms can expand from
// them - so every deliberately-oversized program in
// KHR-GL4x.shader_image_load_store.uniform-limits linked cleanly and then rendered nothing.
//
// Sized off the ADVERTISED limits rather than a constant, because the numbers come from the
// active backend and the whole point of the check is that the two agree.
TEST_F(ProgramInterfaceTest, ImageUniformsOverAStageLimitFailToLink) {
GLint maxFragmentImages = 0;
GLint maxCombinedImages = 0;
GetIntegerv(GL_MAX_FRAGMENT_IMAGE_UNIFORMS, &maxFragmentImages);
GetIntegerv(GL_MAX_COMBINED_IMAGE_UNIFORMS, &maxCombinedImages);
ClearErrors();
ASSERT_GT(maxFragmentImages, 0);
// The fragment stage is compiled explicitly so a COMPILE failure can never be mistaken
// for the link failure under test.
const auto linkWithFragmentImages = [](GLint count) {
const std::string n = std::to_string(count);
const std::string source = std::string(R"(#version 430
out vec4 color;
layout(r32i) uniform iimage2D u_image[)") + n + R"(];
void main() {
int value = 1;
for (int i = 0; i < )" + n + R"(; ++i) {
value = imageAtomicAdd(u_image[i], ivec2(0), value);
}
color = vec4(float(value));
}
)";
const char* sourcePtr = source.c_str();
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &sourcePtr, nullptr);
CompileShader(fs);
GLint compiled = 0;
GetShaderiv(fs, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_TRUE) << "the fragment stage with " << count << " image uniforms must compile";
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &kSimpleVs, nullptr);
CompileShader(vs);
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
return program;
};
const GLuint over = linkWithFragmentImages(maxFragmentImages + 1);
GLint status = -1;
GetProgramiv(over, GL_LINK_STATUS, &status);
EXPECT_EQ(status, GL_FALSE);
char log[4096] = "";
GetProgramInfoLog(over, sizeof(log), nullptr, log);
EXPECT_NE(std::string(log).find("GL_MAX_FRAGMENT_IMAGE_UNIFORMS"), std::string::npos)
<< "info log was: " << log;
ClearErrors();
// Exactly AT the limit is legal and must still link: the comparison is strictly
// greater-than, and the conformance suite's combined-stage subcase builds a program that
// fills every stage to its own limit and expects it to link whenever the combined limit
// can hold them.
if (maxFragmentImages <= maxCombinedImages) {
const GLuint atLimit = linkWithFragmentImages(maxFragmentImages);
ExpectLinked(atLimit);
ClearErrors();
}
}
// glGetProgramiv(GL_ACTIVE_ATOMIC_COUNTER_BUFFERS) and glGetActiveAtomicCounterBufferiv are
// the pre-4.3 spelling of the interface above, and the spec requires the two to agree.
// Neither did: the first counted glslang's atomic counter UNIFORMS - zero, because the
// relaxed parse folds every atomic_uint into a storage block before reflection runs - and
// the second was a stub that wrote nothing and raised nothing.
TEST_F(ProgramInterfaceTest, ActiveAtomicCounterBufferQueriesMatchTheInterface) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 1, offset = 0) uniform atomic_uint a;
layout (binding = 2, offset = 0) uniform atomic_uint b;
layout (binding = 2, offset = 4) uniform atomic_uint c;
void main() {
color = vec4(float(atomicCounterIncrement(a) + atomicCounterIncrement(b) + atomicCounterIncrement(c)));
}
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
GLint bufferCount = -12345;
GetProgramiv(p, GL_ACTIVE_ATOMIC_COUNTER_BUFFERS, &bufferCount);
EXPECT_EQ(bufferCount, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
ASSERT_EQ(bufferCount, 2);
const auto activeBufferiv = [p](GLuint index, GLenum pname) {
GLint value = -12345;
GetActiveAtomicCounterBufferiv(p, index, pname, &value);
return value;
};
for (GLuint index = 0; index < static_cast<GLuint>(bufferCount); ++index) {
const std::vector<GLint> viaInterface =
Props(p, GL_ATOMIC_COUNTER_BUFFER, index,
{GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE, GL_NUM_ACTIVE_VARIABLES,
GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER});
ASSERT_EQ(viaInterface.size(), 5u);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_BINDING), viaInterface[0]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE), viaInterface[1]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS), viaInterface[2]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER), viaInterface[3]);
EXPECT_EQ(activeBufferiv(index, GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER), viaInterface[4]);
// The counter indices are the GL_UNIFORM indices, in the same order.
const std::vector<GLint> expectedIndices = Props(p, GL_ATOMIC_COUNTER_BUFFER, index, {GL_ACTIVE_VARIABLES});
ASSERT_FALSE(expectedIndices.empty());
std::vector<GLint> indices(expectedIndices.size(), -12345);
GetActiveAtomicCounterBufferiv(p, index, GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES,
indices.data());
EXPECT_EQ(indices, expectedIndices);
}
EXPECT_EQ(TakeError(), GL_NO_ERROR);
GLint sink = -12345;
GetActiveAtomicCounterBufferiv(p, static_cast<GLuint>(bufferCount), GL_ATOMIC_COUNTER_BUFFER_BINDING, &sink);
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
EXPECT_EQ(sink, -12345) << "a rejected query must not write the caller's output";
// The interface-query spelling of the same property is NOT accepted here.
GetActiveAtomicCounterBufferiv(p, 0, GL_BUFFER_BINDING, &sink);
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
EXPECT_EQ(sink, -12345);
}
// --------------------------------------------------------- transform-feedback ------
TEST_F(ProgramInterfaceTest, TransformFeedbackVaryingTypes) {
const char* vs = R"(#version 430
in vec4 position;
flat out ivec4 a;
out float b[2];
flat out uvec2 c;
flat out uint d;
out vec3 e[2];
flat out int f;
void main(void) {
a = ivec4(1); b[0] = 1.1; b[1] = 1.1; c = uvec2(1u); d = 1u;
e[0] = vec3(1.1); e[1] = vec3(1.1); f = 1;
gl_Position = position;
}
)";
const GLuint p = MakeProgram(vs, kSimpleFs);
const char* varyings[6] = {"a", "b[0]", "b[1]", "c", "d", "e"};
TransformFeedbackVaryings(p, 6, varyings, GL_INTERLEAVED_ATTRIBS);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_TRANSFORM_FEEDBACK_VARYING, GL_ACTIVE_RESOURCES), 6);
EXPECT_EQ(Interfaceiv(p, GL_TRANSFORM_FEEDBACK_VARYING, GL_MAX_NAME_LENGTH), 5);
for (const char* name : varyings) ExpectResource(p, GL_TRANSFORM_FEEDBACK_VARYING, name, name);
const std::vector<GLenum> props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE};
EXPECT_EQ(PropsOf(p, GL_TRANSFORM_FEEDBACK_VARYING, "a", props),
(std::vector<GLint>{2, GL_INT_VEC4, 1}));
EXPECT_EQ(PropsOf(p, GL_TRANSFORM_FEEDBACK_VARYING, "b[0]", props), (std::vector<GLint>{5, GL_FLOAT, 1}));
EXPECT_EQ(PropsOf(p, GL_TRANSFORM_FEEDBACK_VARYING, "c", props),
(std::vector<GLint>{2, GL_UNSIGNED_INT_VEC2, 1}));
EXPECT_EQ(PropsOf(p, GL_TRANSFORM_FEEDBACK_VARYING, "e", props),
(std::vector<GLint>{2, GL_FLOAT_VEC3, 2}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
TEST_F(ProgramInterfaceTest, TransformFeedbackPseudoVaryingsAreEnumeratedButUnnamed) {
const char* vs = R"(#version 430
in vec4 position;
out ivec4 a; out uvec2 c; out uint d; out int f; out uint e; out int g;
void main(void) {
a = ivec4(1); c = uvec2(1u); d = 1u; f = 1; e = 1u; g = 1;
gl_Position = position;
}
)";
const GLuint p = MakeProgram(vs, kSimpleFs);
const char* varyings[11] = {"a", "gl_NextBuffer", "c", "gl_SkipComponents1", "d", "gl_SkipComponents2",
"f", "gl_SkipComponents3", "e", "gl_SkipComponents4", "g"};
TransformFeedbackVaryings(p, 11, varyings, GL_INTERLEAVED_ATTRIBS);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
// The capture list drops the layout controls; the interface must not.
EXPECT_EQ(Interfaceiv(p, GL_TRANSFORM_FEEDBACK_VARYING, GL_ACTIVE_RESOURCES), 11);
EXPECT_EQ(Interfaceiv(p, GL_TRANSFORM_FEEDBACK_VARYING, GL_MAX_NAME_LENGTH), 19);
std::vector<std::string> names;
for (GLuint i = 0; i < 11; ++i) names.push_back(ResourceName(p, GL_TRANSFORM_FEEDBACK_VARYING, i));
const auto indexOf = [&names](const std::string& name) -> GLuint {
for (GLuint i = 0; i < names.size(); ++i) {
if (names[i] == name) return i;
}
return GL_INVALID_INDEX;
};
const std::vector<GLenum> props = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE};
ASSERT_NE(indexOf("gl_NextBuffer"), GL_INVALID_INDEX);
EXPECT_EQ(Props(p, GL_TRANSFORM_FEEDBACK_VARYING, indexOf("gl_NextBuffer"), props),
(std::vector<GLint>{14, GL_NONE, 0}));
EXPECT_EQ(Props(p, GL_TRANSFORM_FEEDBACK_VARYING, indexOf("gl_SkipComponents1"), props),
(std::vector<GLint>{19, GL_NONE, 1}));
EXPECT_EQ(Props(p, GL_TRANSFORM_FEEDBACK_VARYING, indexOf("gl_SkipComponents4"), props),
(std::vector<GLint>{19, GL_NONE, 4}));
// ...but they cannot be looked up by name.
for (const char* name : {"gl_NextBuffer", "gl_SkipComponents1", "gl_SkipComponents4"}) {
EXPECT_EQ(GetProgramResourceIndex(p, GL_TRANSFORM_FEEDBACK_VARYING, name), GL_INVALID_INDEX) << name;
}
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------------- uniform-block-types --
// GL_REFERENCED_BY_*_SHADER is per block INSTANCE. An array of uniform blocks enumerates
// one resource per element, and only the elements a stage actually dereferences are
// referenced by it - declaring the array is not referencing every element.
TEST_F(ProgramInterfaceTest, ArrayedUniformBlockReferencedByIsPerElement) {
const char* vs = R"(#version 430
in vec4 position;
uniform SimpleBlock { mat3x2 a; mat4 b; vec4 c; };
void main(void) {
float tmp = a[0][1] * b[1][2] * c.x;
gl_Position = position * tmp;
}
)";
const char* fs = R"(#version 430
uniform TrickyBlock { mat4 b; uint c; } e[2];
out vec4 color;
void main() { color = vec4(0, 1, 0, 1) * e[0].b[0][0]; }
)";
const GLuint p = MakeProgram(vs, fs);
BindAttribLocation(p, 0, "position");
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES), 3);
ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock[0]", "TrickyBlock[0]");
ExpectResource(p, GL_UNIFORM_BLOCK, "TrickyBlock[1]", "TrickyBlock[1]");
const std::vector<GLenum> refProps = {GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER};
EXPECT_EQ(PropsOf(p, GL_UNIFORM_BLOCK, "SimpleBlock", refProps), (std::vector<GLint>{1, 0}));
EXPECT_EQ(PropsOf(p, GL_UNIFORM_BLOCK, "TrickyBlock[0]", refProps), (std::vector<GLint>{0, 1}));
EXPECT_EQ(PropsOf(p, GL_UNIFORM_BLOCK, "TrickyBlock[1]", refProps), (std::vector<GLint>{0, 0}))
<< "the unreferenced element of a block array must not inherit its sibling's stages";
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// The boundary of the rule above, and the case that caught it on device
// (KHR-GL43.program_interface_query.ssb-types): a SHADER STORAGE block array's buffer
// variables reflect under ONE subscript-free spelling shared by every element, so a union
// over them credits element 0 and starves every other element - even the ones the shader
// plainly reads. Storage blocks keep glslang's own mask, and both elements here must report
// the fragment stage.
TEST_F(ProgramInterfaceTest, ArrayedStorageBlockKeepsGlslangStagesForEveryElement) {
const char* vs = R"(#version 430
in vec4 position;
void main(void) { gl_Position = position; }
)";
const char* fs = R"(#version 430
layout(binding = 4) buffer SimpleStorage { vec4 a; } ss[2];
out vec4 color;
void main() { color = ss[0].a + ss[1].a; }
)";
const GLuint p = MakeProgram(vs, fs);
BindAttribLocation(p, 0, "position");
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
const std::vector<GLenum> refProps = {GL_REFERENCED_BY_VERTEX_SHADER, GL_REFERENCED_BY_FRAGMENT_SHADER};
for (const char* name : {"SimpleStorage[0]", "SimpleStorage[1]"}) {
EXPECT_EQ(PropsOf(p, GL_SHADER_STORAGE_BLOCK, name, refProps), (std::vector<GLint>{0, 1}))
<< "for " << name << ": a storage block the fragment stage reads must say so";
}
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// --------------------------------------------------------- separate-programs-vertex --
// A vertex-only separable program's OUTPUT interface is its own stage outputs, and an
// inter-stage block enumerates as its members: "Color.r", and "gl_Position" for the
// anonymous gl_PerVertex redeclaration - never the block instance name "vs_color".
TEST_F(ProgramInterfaceTest, SeparableVertexProgramEnumeratesItsOwnOutputBlockMembers) {
const char* vs = R"(#version 430 core
layout(location = 0) in vec4 in_vertex;
out Color { float r, g, b; vec4 iLikePie; } vs_color;
out gl_PerVertex { vec4 gl_Position; };
uniform float u;
uniform vec4 v;
void main() {
gl_Position = in_vertex;
vs_color.r = u; vs_color.g = 0.0; vs_color.b = 0.0; vs_color.iLikePie = v;
}
)";
const GLuint p = CreateShaderProgramv(GL_VERTEX_SHADER, 1, &vs);
ExpectLinked(p);
ClearErrors();
// Exactly 5: the four Color members plus gl_Position. The anonymous gl_PerVertex
// redeclaration drops gl_PointSize and gl_ClipDistance, which glslang keeps in the
// block type as hidden members rather than erasing.
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 10);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 5);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 15);
ExpectResource(p, GL_PROGRAM_INPUT, "in_vertex", "in_vertex");
for (const char* name : {"Color.r", "Color.g", "Color.b", "Color.iLikePie", "gl_Position"}) {
ExpectResource(p, GL_PROGRAM_OUTPUT, name, name);
}
EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_OUTPUT, "vs_color"), GL_INVALID_INDEX)
<< "the block instance is not the resource; its members are";
// A vertex-stage output has no color number, hence no index either, and it is not
// per-patch. NAME_LENGTH/TYPE/ARRAY_SIZE come from the member, not the block.
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "Color.iLikePie",
{GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_REFERENCED_BY_COMPUTE_SHADER,
GL_REFERENCED_BY_FRAGMENT_SHADER, GL_REFERENCED_BY_GEOMETRY_SHADER,
GL_REFERENCED_BY_TESS_CONTROL_SHADER, GL_REFERENCED_BY_TESS_EVALUATION_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER, GL_IS_PER_PATCH, GL_LOCATION_INDEX}),
(std::vector<GLint>{15, GL_FLOAT_VEC4, 1, 0, 0, 0, 0, 0, 1, 0, -1}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------- separate-programs-geometry --
// Both boundaries are a middle stage here. The input block carries an instance name
// (gl_in[]) so its members are prefixed with the BLOCK name, and the arrayed-ness of the
// block itself is dropped: "gl_PerVertex.gl_Position", array size 1.
TEST_F(ProgramInterfaceTest, SeparableGeometryProgramEnumeratesBothBlockBoundaries) {
const char* gs = R"(#version 430
layout(triangles) in;
layout(triangle_strip, max_vertices = 4) out;
out gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; };
in gl_PerVertex { vec4 gl_Position; float gl_PointSize; float gl_ClipDistance[]; } gl_in[];
void main() {
gl_Position = vec4(-1, 1, 0, 1); EmitVertex();
gl_Position = gl_in[0].gl_Position; EmitVertex();
EndPrimitive();
}
)";
const GLuint p = CreateShaderProgramv(GL_GEOMETRY_SHADER, 1, &gs);
ExpectLinked(p);
ClearErrors();
ExpectResource(p, GL_PROGRAM_INPUT, "gl_PerVertex.gl_Position", "gl_PerVertex.gl_Position");
ExpectResource(p, GL_PROGRAM_OUTPUT, "gl_Position", "gl_Position");
// A non-fragment stage's outputs are varyings: no color number, so no color index.
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_Position", {GL_LOCATION_INDEX}), (std::vector<GLint>{-1}));
const std::vector<GLenum> stageProps = {
GL_NAME_LENGTH,
GL_TYPE,
GL_ARRAY_SIZE,
GL_REFERENCED_BY_COMPUTE_SHADER,
GL_REFERENCED_BY_FRAGMENT_SHADER,
GL_REFERENCED_BY_GEOMETRY_SHADER,
GL_REFERENCED_BY_TESS_CONTROL_SHADER,
GL_REFERENCED_BY_TESS_EVALUATION_SHADER,
GL_REFERENCED_BY_VERTEX_SHADER,
GL_IS_PER_PATCH};
EXPECT_EQ(PropsOf(p, GL_PROGRAM_INPUT, "gl_PerVertex.gl_Position", stageProps),
(std::vector<GLint>{25, GL_FLOAT_VEC4, 1, 0, 0, 1, 0, 0, 0, 0}));
EXPECT_EQ(PropsOf(p, GL_PROGRAM_OUTPUT, "gl_Position", stageProps),
(std::vector<GLint>{12, GL_FLOAT_VEC4, 1, 0, 0, 1, 0, 0, 0, 0}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------- separate-programs-fragment --
TEST_F(ProgramInterfaceTest, SeparableFragmentProgramSeparatesUniformsFromBufferVariables) {
const char* fs = R"(#version 430
out vec4 fs_color;
layout(location = 1) uniform vec4 x;
layout(binding = 0) buffer SimpleBuffer { vec4 a; };
in vec4 vs_color;
void main() { fs_color = vs_color + x + a; }
)";
const GLuint p = CreateShaderProgramv(GL_FRAGMENT_SHADER, 1, &fs);
ExpectLinked(p);
ClearErrors();
// GL_PROGRAM_INPUT is the input interface of the program's FIRST stage, which for a
// separable fragment program is the fragment stage: "vs_color", name length 9.
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NAME_LENGTH), 9);
EXPECT_EQ(GetProgramResourceIndex(p, GL_PROGRAM_INPUT, "vs_color"), 0u);
EXPECT_EQ(ResourceName(p, GL_PROGRAM_INPUT, 0), "vs_color");
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_PROGRAM_OUTPUT, GL_MAX_NAME_LENGTH), 9);
// The buffer variable is NOT a uniform, even though the frontend reflection keeps
// both in one list.
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM, GL_MAX_NAME_LENGTH), 2);
EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_BUFFER_VARIABLE, GL_MAX_NAME_LENGTH), 2);
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 1);
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NAME_LENGTH), 13);
EXPECT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 1);
ExpectResource(p, GL_PROGRAM_OUTPUT, "fs_color", "fs_color");
ExpectResource(p, GL_UNIFORM, "x", "x");
ExpectResource(p, GL_SHADER_STORAGE_BLOCK, "SimpleBuffer", "SimpleBuffer");
ExpectResource(p, GL_BUFFER_VARIABLE, "a", "a");
EXPECT_EQ(GetProgramResourceLocation(p, GL_UNIFORM, "x"), 1);
const GLuint block = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "SimpleBuffer");
const GLuint variable = GetProgramResourceIndex(p, GL_BUFFER_VARIABLE, "a");
EXPECT_EQ(Props(p, GL_SHADER_STORAGE_BLOCK, block,
{GL_NAME_LENGTH, GL_BUFFER_BINDING, GL_NUM_ACTIVE_VARIABLES, GL_REFERENCED_BY_COMPUTE_SHADER,
GL_REFERENCED_BY_FRAGMENT_SHADER, GL_REFERENCED_BY_VERTEX_SHADER, GL_ACTIVE_VARIABLES}),
(std::vector<GLint>{13, 0, 1, 0, 1, 0, static_cast<GLint>(variable)}));
EXPECT_EQ(Props(p, GL_BUFFER_VARIABLE, variable,
{GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_BLOCK_INDEX, GL_ARRAY_STRIDE, GL_IS_ROW_MAJOR,
GL_REFERENCED_BY_FRAGMENT_SHADER, GL_TOP_LEVEL_ARRAY_SIZE, GL_TOP_LEVEL_ARRAY_STRIDE}),
(std::vector<GLint>{2, GL_FLOAT_VEC4, 1, static_cast<GLint>(block), 0, 0, 1, 1, 0}));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------- error handling ----
TEST_F(ProgramInterfaceTest, UnlinkedProgramHasZeroResourcesAndRaisesNoError) {
const GLuint p = CreateProgram();
ClearErrors();
for (const GLenum iface : {GL_PROGRAM_INPUT, GL_PROGRAM_OUTPUT, GL_UNIFORM, GL_UNIFORM_BLOCK,
GL_BUFFER_VARIABLE, GL_SHADER_STORAGE_BLOCK, GL_TRANSFORM_FEEDBACK_VARYING,
GL_VERTEX_SUBROUTINE, GL_FRAGMENT_SUBROUTINE_UNIFORM}) {
EXPECT_EQ(Interfaceiv(p, iface, GL_ACTIVE_RESOURCES), 0) << std::hex << iface;
EXPECT_EQ(Interfaceiv(p, iface, GL_MAX_NAME_LENGTH), 0) << std::hex << iface;
EXPECT_EQ(GetProgramResourceIndex(p, iface, ""), GL_INVALID_INDEX) << std::hex << iface;
}
EXPECT_EQ(Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES), 0);
EXPECT_EQ(Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_MAX_NUM_ACTIVE_VARIABLES), 0);
EXPECT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_MAX_NUM_ACTIVE_VARIABLES), 0);
// Not one stray error - a leftover here aborts the caller's next query.
EXPECT_EQ(TakeError(), GL_NO_ERROR);
// Locations, however, really do require a successful link.
EXPECT_EQ(GetProgramResourceLocation(p, GL_PROGRAM_INPUT, "pie"), -1);
EXPECT_EQ(TakeError(), GL_INVALID_OPERATION);
EXPECT_EQ(GetProgramResourceLocationIndex(p, GL_PROGRAM_OUTPUT, "pie"), -1);
EXPECT_EQ(TakeError(), GL_INVALID_OPERATION);
}
TEST_F(ProgramInterfaceTest, ErrorConditions) {
const GLuint p = MakeProgram(kSimpleVs, kSimpleFs);
BindAttribLocation(p, 0, "position");
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
GLint value = 0;
GLsizei length = 0;
GLchar name[100] = {'\0'};
// <program> is not a name at all.
GetProgramInterfaceiv(1337u, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES, &value);
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
GetProgramResourceIndex(1337u, GL_PROGRAM_INPUT, "pie");
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
GetProgramResourceLocation(1337u, GL_PROGRAM_INPUT, "pie");
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
// <program> names a shader object.
const GLuint shader = CreateShader(GL_FRAGMENT_SHADER);
GetProgramInterfaceiv(shader, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES, &value);
EXPECT_EQ(TakeError(), GL_INVALID_OPERATION);
GetProgramResourceIndex(shader, GL_PROGRAM_INPUT, "pie");
EXPECT_EQ(TakeError(), GL_INVALID_OPERATION);
// <index> past the end.
GetProgramResourceName(p, GL_PROGRAM_INPUT, 3000, 1024, &length, name);
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
// propCount == 0.
GLenum props[1] = {GL_NAME_LENGTH};
GetProgramResourceiv(p, GL_PROGRAM_INPUT, 0, 0, props, 1024, &length, &value);
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
// Negative sizes.
GetProgramResourceName(p, GL_PROGRAM_INPUT, 0, -100, nullptr, name);
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
GetProgramResourceiv(p, GL_PROGRAM_INPUT, 0, 1, props, -100, &length, &value);
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
// A prop this command does not know at all vs. one the interface does not carry.
GLenum unknownProp[1] = {GL_TEXTURE_1D};
GetProgramResourceiv(p, GL_PROGRAM_INPUT, 0, 1, unknownProp, 1024, &length, &value);
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
GLenum wrongInterfaceProp[1] = {GL_OFFSET};
GetProgramResourceiv(p, GL_PROGRAM_INPUT, 0, 1, wrongInterfaceProp, 1024, &length, &value);
EXPECT_EQ(TakeError(), GL_INVALID_OPERATION);
// GL_ATOMIC_COUNTER_BUFFER has no names, and no locations.
GetProgramResourceName(p, GL_ATOMIC_COUNTER_BUFFER, 0, 1024, &length, name);
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
GetProgramResourceLocation(p, GL_ATOMIC_COUNTER_BUFFER, "position");
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
// ...and GetProgramInterfaceiv rejects a pname the interface does not answer.
GetProgramInterfaceiv(p, GL_PROGRAM_INPUT, GL_MAX_NUM_ACTIVE_VARIABLES, &value);
EXPECT_EQ(TakeError(), GL_INVALID_OPERATION);
}
TEST_F(ProgramInterfaceTest, BufSizeIsRespected) {
const char* vs = R"(#version 430
in vec4 position;
uniform vec4 someLongName;
void main(void) { gl_Position = position + someLongName; }
)";
const GLuint p = MakeProgram(vs, kSimpleFs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
const GLuint index = GetProgramResourceIndex(p, GL_UNIFORM, "someLongName");
ASSERT_NE(index, GL_INVALID_INDEX);
GLchar buffer[3] = {'a', 'b', 'c'};
GLsizei length = -1;
GetProgramResourceName(p, GL_UNIFORM, index, 0, nullptr, nullptr);
GetProgramResourceName(p, GL_UNIFORM, index, 0, nullptr, buffer);
EXPECT_EQ(buffer[0], 'a');
EXPECT_EQ(buffer[2], 'c');
GetProgramResourceName(p, GL_UNIFORM, index, 2, &length, buffer);
EXPECT_EQ(buffer[0], 's');
EXPECT_EQ(buffer[1], '\0');
EXPECT_EQ(buffer[2], 'c');
EXPECT_EQ(length, 1);
GLint params[3] = {1, 2, 3};
const GLenum props[] = {GL_NAME_LENGTH, GL_TYPE, GL_ARRAY_SIZE, GL_OFFSET, GL_BLOCK_INDEX, GL_LOCATION};
GetProgramResourceiv(p, GL_UNIFORM, index, 6, props, 0, nullptr, nullptr);
GetProgramResourceiv(p, GL_UNIFORM, index, 6, props, 0, nullptr, params);
EXPECT_EQ(params[0], 1);
EXPECT_EQ(params[2], 3);
GetProgramResourceiv(p, GL_UNIFORM, index, 6, props, 2, &length, params);
EXPECT_EQ(params[0], 13);
EXPECT_EQ(params[1], GL_FLOAT_VEC4);
EXPECT_EQ(params[2], 3);
EXPECT_EQ(length, 2);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ------------------------------------------------------------- buffer binding state ----
// GL 4.6 §7.3.1 makes ONE index space out of glGetProgramResourceIndex and the command
// that consumes its answer: the index glGetProgramResourceIndex(GL_SHADER_STORAGE_BLOCK)
// returns IS the index glShaderStorageBlockBinding takes. The declared bindings below are
// deliberately NOT the enumeration order, so a binding applied through a different index
// space lands on the wrong block instead of failing loudly.
const char* kStorageBlockFs = R"(#version 430
layout(binding = 3) buffer BlockA { vec4 a; };
layout(binding = 1) buffer BlockB { vec4 b; };
layout(binding = 2) buffer BlockC { vec4 c; };
out vec4 color;
void main() { color = a + b + c; }
)";
// A binding read straight back through GL_BUFFER_BINDING, by NAME, so the assertion does
// not depend on the enumeration order it is meant to be checking.
GLint BufferBindingOf(GLuint program, GLenum iface, const char* name) {
const std::vector<GLint> values = PropsOf(program, iface, name, {GL_BUFFER_BINDING});
return values.size() == 1 ? values[0] : -12345;
}
TEST_F(ProgramInterfaceTest, ShaderStorageBlockBindingTakesTheResourceQueryIndex) {
const GLuint p = MakeProgram(kSimpleVs, kStorageBlockFs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
// Bound first, the way a real caller reaches glShaderStorageBlockBinding. It matters
// because that entry point also delegates to the backend, and asking a BACKEND to
// build a program for the first time from inside a non-draw entry point is a
// pre-existing DirectGLES hazard (GetBackendProgramId -> SyncToBackend runs without
// the draw-path globals SyncCurrentProgram would have established; under a loaded
// llvmpipe it throws out of the transpile). Nothing about the index space under test
// depends on this - it just keeps the case testing the frontend contract.
UseProgram(p);
ASSERT_EQ(Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES), 3);
// Until something rebinds them, GL_BUFFER_BINDING is what the shader declared.
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockA"), 3);
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockB"), 1);
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockC"), 2);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
// THE ROUND TRIP. BlockB is the interesting one: its enumeration index (1) and its
// declared binding (1) coincide, while BlockA's do not, so an implementation that
// confused index with binding would still pass on B alone - hence all three are
// re-read afterwards.
const GLuint blockA = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "BlockA");
ASSERT_NE(blockA, GL_INVALID_INDEX);
ShaderStorageBlockBinding(p, blockA, 6);
// GPU-free suite: with no backend bound the call still records the binding on the
// program (that is the state GL_BUFFER_BINDING reports) and then reports that it
// could not reach a driver. Swallow exactly that.
ClearErrors();
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockA"), 6) << "the rebound block";
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockB"), 1) << "must not move";
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockC"), 2) << "must not move";
EXPECT_EQ(TakeError(), GL_NO_ERROR);
// And it survives a second, different rebinding of another block.
const GLuint blockC = GetProgramResourceIndex(p, GL_SHADER_STORAGE_BLOCK, "BlockC");
ASSERT_NE(blockC, GL_INVALID_INDEX);
ShaderStorageBlockBinding(p, blockC, 0);
ClearErrors();
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockA"), 6);
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockB"), 1);
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockC"), 0);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
TEST_F(ProgramInterfaceTest, ShaderStorageBlockBindingRejectsAnIndexOutsideTheInterface) {
const GLuint p = MakeProgram(kSimpleVs, kStorageBlockFs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
const GLint activeBlocks = Interfaceiv(p, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES);
ASSERT_EQ(activeBlocks, 3);
ShaderStorageBlockBinding(p, static_cast<GLuint>(activeBlocks), 4);
EXPECT_EQ(TakeError(), GL_INVALID_VALUE);
ClearErrors();
// Nothing moved.
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockA"), 3);
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockB"), 1);
EXPECT_EQ(BufferBindingOf(p, GL_SHADER_STORAGE_BLOCK, "BlockC"), 2);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// The same rule on the uniform side: GL_BUFFER_BINDING is the CURRENT binding, and
// GL_UNIFORM_BLOCK's index space is the one glGetUniformBlockIndex / glUniformBlockBinding
// already use.
TEST_F(ProgramInterfaceTest, UniformBlockBufferBindingFollowsUniformBlockBinding) {
const char* fs = R"(#version 430
layout(binding = 2) uniform BlockU { vec4 u; };
layout(binding = 0) uniform BlockV { vec4 v; };
out vec4 color;
void main() { color = u + v; }
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
ASSERT_EQ(Interfaceiv(p, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES), 2);
EXPECT_EQ(BufferBindingOf(p, GL_UNIFORM_BLOCK, "BlockU"), 2);
EXPECT_EQ(BufferBindingOf(p, GL_UNIFORM_BLOCK, "BlockV"), 0);
// One index space, both directions.
const GLuint interfaceIndex = GetProgramResourceIndex(p, GL_UNIFORM_BLOCK, "BlockU");
ASSERT_NE(interfaceIndex, GL_INVALID_INDEX);
EXPECT_EQ(interfaceIndex, GetUniformBlockIndex(p, "BlockU"));
EXPECT_EQ(TakeError(), GL_NO_ERROR);
UniformBlockBinding(p, interfaceIndex, 5);
ClearErrors();
EXPECT_EQ(BufferBindingOf(p, GL_UNIFORM_BLOCK, "BlockU"), 5) << "the rebound block";
EXPECT_EQ(BufferBindingOf(p, GL_UNIFORM_BLOCK, "BlockV"), 0) << "must not move";
// glGetActiveUniformBlockiv is the older spelling of the same state; the two must not
// be able to disagree.
GLint viaActiveUniformBlockiv = -1;
GetActiveUniformBlockiv(p, interfaceIndex, GL_UNIFORM_BLOCK_BINDING, &viaActiveUniformBlockiv);
EXPECT_EQ(viaActiveUniformBlockiv, 5);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// ---------------------------------------------------- queries on an unlinked program ----
// glGetProgramiv is legal on a program that has never linked - GL 4.6 sec. 7.3 says the
// queried state simply has its initial value - but the reflection-backed pnames read
// Artifacts().program, which is null until a link produces one. That dereference was a
// SIGSEGV inside glslang::TProgram::getNumPipeInputs, and KHR-GL30.api.coverage walks into it
// (it queries GL_ACTIVE_ATTRIBUTES right after a glGetAttribLocation that failed). It only
// became reachable once the glCopyTexImage2D throw ahead of it in the same case stopped
// killing the run first.
TEST_F(ProgramInterfaceTest, ReflectionQueriesOnAnUnlinkedProgramAnswerZero) {
const GLuint neverLinked = CreateProgram();
ASSERT_NE(neverLinked, 0u);
ClearErrors();
for (const GLenum pname : {GL_ACTIVE_ATTRIBUTES, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, GL_ACTIVE_UNIFORMS,
GL_ACTIVE_UNIFORM_MAX_LENGTH, GL_ACTIVE_UNIFORM_BLOCKS,
GL_ACTIVE_ATOMIC_COUNTER_BUFFERS}) {
GLint value = -1;
GetProgramiv(neverLinked, pname, &value);
ClearErrors();
EXPECT_GE(value, 0) << "pname 0x" << std::hex << pname << " left its output untouched";
}
// A program that was linked and FAILED is the shape api.coverage actually hits.
const GLuint brokenSource = MakeProgram("#version 430\nvoid main() { this is not glsl }\n", kSimpleFs);
LinkProgram(brokenSource);
ClearErrors();
GLint linked = GL_TRUE;
GetProgramiv(brokenSource, GL_LINK_STATUS, &linked);
ASSERT_EQ(linked, GL_FALSE) << "the shader was supposed to fail to compile";
ClearErrors();
GLint attributes = -1;
GetProgramiv(brokenSource, GL_ACTIVE_ATTRIBUTES, &attributes);
ClearErrors();
EXPECT_EQ(attributes, 0);
// GL_COMPUTE_WORK_GROUP_SIZE is GL_INVALID_OPERATION on a program that has not linked (GL
// 4.6 sec. 7.13), so it is allowed to leave the output alone - but it still reaches
// GetComputeLocalSize(), and it may not do so through a null reflection.
GLint localSize[3] = {-1, -1, -1};
GetProgramiv(brokenSource, GL_COMPUTE_WORK_GROUP_SIZE, localSize);
const GLenum computeError = TakeError();
ClearErrors();
EXPECT_TRUE(computeError == GL_INVALID_OPERATION || (localSize[0] == 0 && localSize[1] == 0 &&
localSize[2] == 0))
<< "either the query is refused, or it answers the initial value - never both untouched "
"and unreported";
}
// ------------------------------------------------------------- length on every path ----
// glGetProgramResourceiv's *length is the caller's only signal for how many entries params
// holds, and callers are entitled to leave it uninitialised: the CTS declares `GLsizei
// length;` next to a 1000-entry stack array and then loops `for (i = 0; i < length; ++i)`
// (gl4cProgramInterfaceQueryTests.cpp:2172). Leaving it untouched on an error path therefore
// does not "return nothing" - it hands the caller whatever was on its stack and makes it walk
// that far. KHR-GL43.program_interface_query.subroutines-vertex read 0x20202020 (" ")
// entries and took the process down on BOTH backends. So: zero on every exit, real count on
// success. Poisoning with the exact CTS-observed value keeps the assertion honest.
TEST_F(ProgramInterfaceTest, GetProgramResourceivReportsLengthOnEveryExitPath) {
const GLuint p = MakeProgram(kSimpleVs, kSimpleFs);
BindAttribLocation(p, 0, "position");
BindFragDataLocation(p, 0, "color");
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
constexpr GLsizei kPoison = 0x20202020;
constexpr GLsizei kBufSize = 16;
GLint params[kBufSize] = {};
const GLenum nameLengthProp = GL_NAME_LENGTH;
const GLenum compatibleSubroutinesProp = GL_COMPATIBLE_SUBROUTINES;
const GLenum notAProp = GL_TEXTURE_2D;
const auto lengthAfter = [&](GLuint program, GLenum iface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLint* out) {
GLsizei length = kPoison;
GetProgramResourceiv(program, iface, index, propCount, props, bufSize, &length, out);
ClearErrors();
return length;
};
// The case that actually crashed: no subroutine reflection exists, so the query errors
// out - and the caller then trusts *length.
EXPECT_EQ(lengthAfter(p, GL_VERTEX_SUBROUTINE_UNIFORM, 0, 1, &compatibleSubroutinesProp, kBufSize, params), 0)
<< "GL_VERTEX_SUBROUTINE_UNIFORM";
// Not a program name.
EXPECT_EQ(lengthAfter(p + 4242, GL_UNIFORM, 0, 1, &nameLengthProp, kBufSize, params), 0) << "bad program";
// Not an interface enum.
EXPECT_EQ(lengthAfter(p, GL_TEXTURE_2D, 0, 1, &nameLengthProp, kBufSize, params), 0) << "bad interface";
// propCount <= 0, bufSize < 0.
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 0, &nameLengthProp, kBufSize, params), 0) << "propCount 0";
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &nameLengthProp, -1, params), 0) << "negative bufSize";
// props == nullptr.
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, nullptr, kBufSize, params), 0) << "null props";
// A prop this command does not know at all.
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &notAProp, kBufSize, params), 0) << "unknown prop";
// A prop it knows but this interface does not carry.
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &compatibleSubroutinesProp, kBufSize, params), 0)
<< "prop/interface mismatch";
// Index past the end of a real interface.
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 9999, 1, &nameLengthProp, kBufSize, params), 0) << "bad index";
// Nowhere to put the values.
EXPECT_EQ(lengthAfter(p, GL_PROGRAM_OUTPUT, 0, 1, &nameLengthProp, kBufSize, nullptr), 0) << "null params";
// ...and the success path still reports the count it actually wrote.
const GLuint outputIndex = GetProgramResourceIndex(p, GL_PROGRAM_OUTPUT, "color");
ASSERT_NE(outputIndex, GL_INVALID_INDEX);
GLsizei length = kPoison;
GetProgramResourceiv(p, GL_PROGRAM_OUTPUT, outputIndex, 1, &nameLengthProp, kBufSize, &length, params);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
EXPECT_EQ(length, 1);
EXPECT_EQ(params[0], 6) << "GL_NAME_LENGTH counts the terminator";
}
} // namespace