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

3242 lines
122 KiB
C++

// MobileGL - MobileGL/MG_Test/Program/ProgramTest.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
#include <gtest/gtest.h>
#include <spirv_reflect.h>
#include <cstring>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
#include "MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h"
#include "MG_Backend/BackendObjects.h"
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/ProgramState/ShaderPreprocessCache.h"
#include "MG_Util/Async/ShaderCompilePool.h"
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
class ProgramTest : public ::testing::Test {
protected:
void SetUp() override { MobileGL::Initialize(); }
void TearDown() override {}
};
TEST_F(ProgramTest, Sanity) {
ASSERT_TRUE(true);
}
const char* vsSrc = R"(#version 460
layout (location = 2) in vec4 Position;
in float fIn4;
in float fIn2;
in float fIn5;
in float fIn6;
in float fIn1;
layout (location = 0) in float fIn0;
in float fIn3;
layout(location = 0) uniform mat4 ProjMat;
layout(location = 10) uniform mat3 TestMat3;
layout(location = 20) uniform mat2 TestMat2;
uniform vec2 InSize;
uniform vec2 OutSize;
out vec2 texCoord;
out vec2 oneTexel;
void main(){
vec4 outPos = ProjMat * vec4(Position.xy, 0.0, 1.0);
gl_Position = vec4(outPos.xy, 0.2, 1.0);
// Use TestMat2 and TestMat3 to prevent optimization
vec2 dummy2 = TestMat2[0];
vec3 dummy3 = TestMat3[0];
oneTexel = (1.0 * (fIn1 * fIn2 * fIn3 * fIn4 * fIn5 * fIn6 * fIn0)) / InSize;
texCoord = Position.xy / OutSize;
})";
const char* fsSrc = R"(#version 460
uniform sampler2D InSampler;
in vec2 texCoord;
in vec2 oneTexel;
uniform vec2 InSize;
layout(location = 1) uniform vec3 Gray;
uniform vec3 RedMatrix;
uniform vec3 GreenMatrix0;
uniform vec3 BlueMatrix;
uniform vec3 Offset;
uniform vec3 ColorScale;
layout(location = 6) uniform float Saturation;
uniform int AQuickFoxJumpsOverALazyDog;
uniform int intVal;
out vec4 fragColor;
void main() {
vec4 InTexel = texture(InSampler, texCoord);
// Color Matrix
float RedValue = dot(InTexel.rgb, RedMatrix);
float GreenValue = dot(InTexel.rgb, GreenMatrix0);
float BlueValue = dot(InTexel.rgb, BlueMatrix);
vec3 OutColor = vec3(RedValue, GreenValue, BlueValue);
// Offset & Scale
OutColor = (OutColor * ColorScale) + Offset;
// Saturation
float Luma = dot(OutColor, Gray);
vec3 Chroma = OutColor - Luma;
OutColor = (Chroma * Saturation) + Luma;
fragColor = vec4(OutColor, float(intVal));
})";
const char* sodiumStylePushConstantVs = R"(#version 460 core
layout(location = 0) in vec3 Position;
#ifdef VULKAN
layout(push_constant) uniform PC {
vec3 u_RegionOffset;
int u_CurrentTime;
uint u_RegionID;
};
#else
uniform vec3 u_RegionOffset;
uniform int u_CurrentTime;
uniform uint u_RegionID;
#endif
void main() {
vec3 offset = u_RegionOffset + vec3(float(u_CurrentTime) * 0.0 + float(u_RegionID) * 0.0);
gl_Position = vec4(Position + offset, 1.0);
})";
const char* sodiumStylePushConstantFs = R"(#version 460 core
out vec4 fragColor;
void main() {
fragColor = vec4(1.0);
})";
TEST_F(ProgramTest, CompileVertex) {
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSrc, NULL);
CompileShader(vs);
}
TEST_F(ProgramTest, CompileFragment) {
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &fsSrc, NULL);
CompileShader(fs);
}
TEST_F(ProgramTest, CompileVoxySubgroupProbeShader) {
char infoLog[1024] = "";
const char* csSrc = R"(#version 430
#extension GL_KHR_shader_subgroup_basic : require
#extension GL_KHR_shader_subgroup_arithmetic : require
layout(local_size_x=32) in;
void main() {
uint a = subgroupExclusiveAdd(gl_LocalInvocationIndex);
}
)";
GLuint cs = CreateShader(GL_COMPUTE_SHADER);
ShaderSource(cs, 1, &csSrc, nullptr);
CompileShader(cs);
GLint compileStatus = GL_FALSE;
GetShaderiv(cs, GL_COMPILE_STATUS, &compileStatus);
GetShaderInfoLog(cs, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(compileStatus, GL_TRUE) << infoLog;
}
TEST_F(ProgramTest, CompileVoxyGpuShaderInt64QuadDecode) {
auto previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_Backend::pActiveBackendObject = MakeUnique<MG_Backend::DirectVulkan::BackendObject_DirectVulkan>();
char infoLog[2048] = "";
const char* vsSrc = R"(#version 460 core
#extension GL_ARB_gpu_shader_int64 : enable
#ifdef GL_ARB_gpu_shader_int64
#define Quad uint64_t
#define Eu32(data, amountBits, shift) (uint((data)>>(shift))&((1u<<(amountBits))-1))
vec3 extractPos(uint64_t quad) {
return vec3(Eu32(quad, 5, 21), Eu32(quad, 5, 16), Eu32(quad, 5, 11));
}
uint extractStateId(uint64_t quad) {
return Eu32(quad, 16, 26);
}
uint extractBiomeId(uint64_t quad) {
return Eu32(quad, 9, 46);
}
#else
#error GL_ARB_gpu_shader_int64 should select Voxy native quad decode path
#endif
layout(std430, binding = 1) readonly buffer QuadBuffer {
Quad quadData[];
};
layout(location = 0) flat out uvec4 interData;
void main() {
uint64_t quad = quadData[uint(gl_VertexID) >> 2];
vec3 pos = extractPos(quad);
interData = uvec4(extractStateId(quad), extractBiomeId(quad), uint(pos.x), uint(pos.y));
gl_Position = vec4(pos * (1.0 / 32.0), 1.0);
}
)";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSrc, nullptr);
CompileShader(vs);
GLint compileStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &compileStatus);
GetShaderInfoLog(vs, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(compileStatus, GL_TRUE) << infoLog;
MG_Backend::pActiveBackendObject = Move(previousBackend);
}
TEST_F(ProgramTest, ShaderSourceKeepsOriginalTextAfterCompile) {
const char* part0 = R"(#define HIGHP_OR_DEFAULT highp
attribute vec4 Position;
varying vec2 uv;
)";
const char* ignored = "this segment should be ignored";
const char* part2 = R"(void main() {
uv = Position.xy;
gl_Position = Position;
}
)";
const GLchar* parts[] = {part0, ignored, part2};
const GLint lengths[] = {static_cast<GLint>(std::strlen(part0)), 0, static_cast<GLint>(std::strlen(part2))};
const String expectedSource = String(part0) + part2;
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 3, parts, lengths);
GLint sourceLength = 0;
GetShaderiv(vs, GL_SHADER_SOURCE_LENGTH, &sourceLength);
ASSERT_EQ(sourceLength, static_cast<GLint>(expectedSource.size() + 1));
std::vector<GLchar> sourceBuffer(static_cast<size_t>(sourceLength));
GLsizei written = 0;
GetShaderSource(vs, sourceLength, &written, sourceBuffer.data());
EXPECT_EQ(written, static_cast<GLsizei>(expectedSource.size()));
EXPECT_EQ(String(sourceBuffer.data(), static_cast<size_t>(written)), expectedSource);
CompileShader(vs);
GLint compileStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &compileStatus);
ASSERT_EQ(compileStatus, GL_TRUE);
std::fill(sourceBuffer.begin(), sourceBuffer.end(), '\0');
written = 0;
GetShaderSource(vs, sourceLength, &written, sourceBuffer.data());
EXPECT_EQ(written, static_cast<GLsizei>(expectedSource.size()));
EXPECT_EQ(String(sourceBuffer.data(), static_cast<size_t>(written)), expectedSource);
}
TEST_F(ProgramTest, LinkProgramWithLegacyGlmarkStyleShaders) {
char infoLog[1024] = "";
const char* legacyVs = R"(attribute vec4 Position;
attribute vec2 TexCoord;
varying vec2 vTexCoord;
void main() {
vTexCoord = TexCoord;
gl_Position = Position;
}
)";
const char* legacyFs = R"(varying vec2 vTexCoord;
uniform sampler2D Texture;
void main() {
gl_FragColor = texture2D(Texture, vTexCoord);
}
)";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &legacyVs, NULL);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, sizeof(infoLog), nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &legacyFs, NULL);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, sizeof(infoLog), nullptr, infoLog);
ASSERT_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);
ASSERT_EQ(linkStatus, GL_TRUE) << infoLog;
}
TEST_F(ProgramTest, ImageUniformLayoutBindingInitializesImageUnit) {
char infoLog[1024] = "";
const char* csSrc = R"(#version 460 core
layout(local_size_x = 1) in;
layout(binding = 4, rgba8) uniform writeonly image2D colourTexOut;
void main() {
imageStore(colourTexOut, ivec2(0), vec4(1.0));
}
)";
GLuint cs = CreateShader(GL_COMPUTE_SHADER);
ShaderSource(cs, 1, &csSrc, nullptr);
CompileShader(cs);
GLint csStatus = GL_FALSE;
GetShaderiv(cs, GL_COMPILE_STATUS, &csStatus);
GetShaderInfoLog(cs, sizeof(infoLog), nullptr, infoLog);
ASSERT_EQ(csStatus, GL_TRUE) << infoLog;
GLuint program = CreateProgram();
AttachShader(program, cs);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
ASSERT_EQ(linkStatus, GL_TRUE) << infoLog;
const GLint location = GetUniformLocation(program, "colourTexOut");
ASSERT_GE(location, 0);
auto programObject = MobileGL::MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
EXPECT_EQ(programObject->GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location)), 4);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, OutOfRangeComputeLocalSizeLiteralFailsCompileInsteadOfThrowing) {
// The layout scanner's digit capture is unbounded, so a literal wider than 64 bits is a
// legal match. It must saturate and be rejected through COMPILE_STATUS; if the integer
// conversion throws instead, the exception escapes glCompileShader entirely.
char infoLog[1024] = "";
const char* csSrc = R"(#version 460 core
layout(local_size_x = 99999999999999999999999) in;
void main() {
}
)";
GLuint cs = CreateShader(GL_COMPUTE_SHADER);
ShaderSource(cs, 1, &csSrc, nullptr);
CompileShader(cs);
GLint csStatus = GL_TRUE;
GetShaderiv(cs, GL_COMPILE_STATUS, &csStatus);
EXPECT_EQ(csStatus, GL_FALSE);
GetShaderInfoLog(cs, sizeof(infoLog), nullptr, infoLog);
EXPECT_NE(String(infoLog).find("GL_MAX_COMPUTE_WORK_GROUP_SIZE"), String::npos) << infoLog;
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, DirectVulkanStorageBlockUsesShaderLayoutBinding) {
char infoLog[1024] = "";
const char* csSrc = R"(#version 460 core
layout(local_size_x = 1) in;
layout(std430, binding = 2) buffer requestQueueStruct {
uint value;
} requestQueue;
void main() {
requestQueue.value = 1u;
}
)";
GLuint cs = CreateShader(GL_COMPUTE_SHADER);
ShaderSource(cs, 1, &csSrc, nullptr);
CompileShader(cs);
GLint csStatus = GL_FALSE;
GetShaderiv(cs, GL_COMPILE_STATUS, &csStatus);
GetShaderInfoLog(cs, sizeof(infoLog), nullptr, infoLog);
ASSERT_EQ(csStatus, GL_TRUE) << infoLog;
GLuint program = CreateProgram();
AttachShader(program, cs);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
ASSERT_EQ(linkStatus, GL_TRUE) << infoLog;
auto programObject = MobileGL::MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
const GLuint blockIndex =
MG_Backend::DirectVulkan::GetShaderStorageBlockIndex(*programObject, "requestQueueStruct");
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
EXPECT_EQ(MG_Backend::DirectVulkan::GetShaderStorageBlockBinding(*programObject, blockIndex), 2u);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, CompileAndLink) {
char infoLog[1024] = "";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSrc, NULL);
printf("Compiling vertex shader: %s\n", vsSrc);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
printf("Compiled vertex shader.\n");
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &fsSrc, NULL);
printf("Compiling fragment shader: %s\n", fsSrc);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
printf("Compiled fragment shader.\n");
GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
BindAttribLocation(program, 1, "fIn1");
BindAttribLocation(program, 3, "fIn3");
BindAttribLocation(program, 5, "fIn5");
printf("Linking program...\n");
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE);
printf("Program linked.\n");
ASSERT_EQ(GetUniformLocation(program, "ProjMat"), 0);
ASSERT_EQ(GetUniformLocation(program, "Gray"), 1);
ASSERT_EQ(GetUniformLocation(program, "Saturation"), 6);
GLint uniformCount = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &uniformCount);
ASSERT_EQ(uniformCount, 14);
GLint uniformNameMaxLength = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &uniformNameMaxLength);
ASSERT_EQ(uniformNameMaxLength, static_cast<GLint>(sizeof("GreenMatrix0")));
ASSERT_EQ(GetAttribLocation(program, "Position"), 2);
ASSERT_EQ(GetAttribLocation(program, "fIn1"), 1);
ASSERT_EQ(GetAttribLocation(program, "fIn3"), 3);
ASSERT_EQ(GetAttribLocation(program, "fIn5"), 5);
ASSERT_EQ(GetAttribLocation(program, "fIn0"), 0);
UseProgram(program);
auto locRed = GetUniformLocation(program, "RedMatrix");
Uniform3f(locRed, 1.0, 3.0, 5.0);
float redVal[3];
GetUniformfv(program, locRed, redVal);
ASSERT_EQ(redVal[0], 1.0);
ASSERT_EQ(redVal[1], 3.0);
ASSERT_EQ(redVal[2], 5.0);
auto locAbc = GetUniformLocation(program, "AQuickFoxJumpsOverALazyDog");
ASSERT_EQ(locAbc, -1);
auto locInt = GetUniformLocation(program, "intVal");
Uniform1i(locInt, 114514);
int intVal;
GetUniformiv(program, locInt, &intVal);
EXPECT_EQ(intVal, 114514);
auto programObj = MG_State::pGLContext->GetProgramObject(program);
auto& shaderSpirvs = programObj->GetGeneratedSpirv();
for (int index = 0; index < shaderSpirvs.size(); ++index) {
String source;
auto& spirvCode = shaderSpirvs[index];
MG_Util::ShaderTranspiler::SpvcSession spvcSession(spirvCode, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
spvc_compiler_options options;
spvcSession.CreateOptions(&options);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
spvcSession.SetOptions(options);
const char* result = nullptr;
spvcSession.Compile(&result);
if (!result) {
MG_Util::ShaderTranspiler::ResultInfo r;
r.log += "Failed to compile the shader to GLSL: \n";
r.log += spvcSession.GetLastErrorString();
r.errc = -5;
FAIL() << r.log;
}
printf("shader dump: \n%s\n", result);
}
}
TEST_F(ProgramTest, SodiumStyleVulkanMacroShaderUsesPlainUniforms) {
char infoLog[1024] = "";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &sodiumStylePushConstantVs, NULL);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, sizeof(infoLog), nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &sodiumStylePushConstantFs, NULL);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, sizeof(infoLog), nullptr, infoLog);
ASSERT_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);
ASSERT_EQ(linkStatus, GL_TRUE) << infoLog;
const GLint regionOffsetLoc = GetUniformLocation(program, "u_RegionOffset");
const GLint currentTimeLoc = GetUniformLocation(program, "u_CurrentTime");
const GLint regionIdLoc = GetUniformLocation(program, "u_RegionID");
ASSERT_GE(regionOffsetLoc, 0);
ASSERT_GE(currentTimeLoc, 0);
ASSERT_GE(regionIdLoc, 0);
auto programObject = MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
ASSERT_GT(programObject->GetUBOSize(), 0u);
UseProgram(program);
Uniform3f(regionOffsetLoc, 1.0f, 2.0f, 3.0f);
Uniform1i(currentTimeLoc, 4);
Uniform1ui(regionIdLoc, 5u);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLfloat regionOffset[3] = {};
GetUniformfv(program, regionOffsetLoc, regionOffset);
EXPECT_EQ(regionOffset[0], 1.0f);
EXPECT_EQ(regionOffset[1], 2.0f);
EXPECT_EQ(regionOffset[2], 3.0f);
}
TEST_F(ProgramTest, Uniform1uiStoresUnsignedValue) {
char infoLog[1024] = "";
const char* simpleVs = R"(#version 460
layout(location = 0) in vec4 Position;
void main() {
gl_Position = Position;
}
)";
const char* uintFs = R"(#version 460
uniform uint NodeQueueIndex;
out vec4 fragColor;
void main() {
fragColor = vec4(float(NodeQueueIndex & 255u));
}
)";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &simpleVs, NULL);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &uintFs, NULL);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_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);
ASSERT_EQ(linkStatus, GL_TRUE);
UseProgram(program);
GLint loc = GetUniformLocation(program, "NodeQueueIndex");
ASSERT_GE(loc, 0);
const GLuint expected = 0xF1234567u;
Uniform1ui(loc, expected);
GLint actual = 0;
GetUniformiv(program, loc, &actual);
EXPECT_EQ(static_cast<GLuint>(actual), expected);
}
TEST_F(ProgramTest, UniformMatrixFunctions) {
char infoLog[1024] = "";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSrc, NULL);
printf("Compiling vertex shader: %s\n", vsSrc);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
printf("Compiled vertex shader.\n");
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &fsSrc, NULL);
printf("Compiling fragment shader: %s\n", fsSrc);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
printf("Compiled fragment shader.\n");
GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
BindAttribLocation(program, 1, "fIn1");
BindAttribLocation(program, 3, "fIn3");
BindAttribLocation(program, 5, "fIn5");
printf("Linking program...\n");
LinkProgram(program);
printf("Program linked.\n");
UseProgram(program);
int uniformCount = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &uniformCount);
ASSERT_LT(uniformCount, 4000);
// Test UniformMatrix2fv
auto locProjMat = GetUniformLocation(program, "ProjMat");
ASSERT_NE(locProjMat, -1);
// 4x4 matrix (16 elements) - identity matrix
GLfloat matrix4x4[16] = {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f};
// Test UniformMatrix4fv with count = 1 and transpose = GL_FALSE
UniformMatrix4fv(locProjMat, 1, GL_FALSE, matrix4x4);
// Test UniformMatrix4fv with count = 1 and transpose = GL_TRUE
UniformMatrix4fv(locProjMat, 1, GL_TRUE, matrix4x4);
// Test with a non-identity matrix
GLfloat nonIdentityMatrix[16] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f,
9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f};
// Test with transpose = GL_FALSE
UniformMatrix4fv(locProjMat, 1, GL_FALSE, nonIdentityMatrix);
// Test with transpose = GL_TRUE
UniformMatrix4fv(locProjMat, 1, GL_TRUE, nonIdentityMatrix);
}
TEST_F(ProgramTest, UniformMatrixTranspose) {
char infoLog[1024] = "";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSrc, NULL);
printf("Compiling vertex shader: %s\n", vsSrc);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
printf("Compiled vertex shader.\n");
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &fsSrc, NULL);
printf("Compiling fragment shader: %s\n", fsSrc);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
printf("Compiled fragment shader.\n");
GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
BindAttribLocation(program, 1, "fIn1");
BindAttribLocation(program, 3, "fIn3");
BindAttribLocation(program, 5, "fIn5");
printf("Linking program...\n");
LinkProgram(program);
printf("Program linked.\n");
UseProgram(program);
int uniformCount = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &uniformCount);
ASSERT_LT(uniformCount, 4000);
// Test 2x2 matrix transpose
auto locMat2 = GetUniformLocation(program, "TestMat2");
ASSERT_NE(locMat2, -1);
// Test matrix (column-major as expected by OpenGL):
// [1 3]
// [2 4]
GLfloat matrix2x2[4] = {
1.0f, 2.0f, // First column
3.0f, 4.0f // Second column
};
// Expected values when transpose = GL_FALSE (no transpose):
// [1 3]
// [2 4]
GLfloat expected2x2_no_transpose[4] = {1.0f, 2.0f, 3.0f, 4.0f};
// Expected values when transpose = GL_TRUE (transposed):
// [1 2]
// [3 4]
// Stored in column-major order: [1, 3, 2, 4]
GLfloat expected2x2_transpose[4] = {1.0f, 3.0f, 2.0f, 4.0f};
// Test with transpose = GL_FALSE
UniformMatrix2fv(locMat2, 1, GL_FALSE, matrix2x2);
GLfloat result2x2_no_transpose[4];
GetUniformfv(program, locMat2, result2x2_no_transpose);
for (int i = 0; i < 4; i++) {
EXPECT_FLOAT_EQ(result2x2_no_transpose[i], expected2x2_no_transpose[i]);
}
// Test with transpose = GL_TRUE
UniformMatrix2fv(locMat2, 1, GL_TRUE, matrix2x2);
GLfloat result2x2_transpose[4];
GetUniformfv(program, locMat2, result2x2_transpose);
for (int i = 0; i < 4; i++) {
EXPECT_FLOAT_EQ(result2x2_transpose[i], expected2x2_transpose[i]);
}
// Test 3x3 matrix transpose
auto locMat3 = GetUniformLocation(program, "TestMat3");
ASSERT_NE(locMat3, -1);
// Test matrix (column-major as expected by OpenGL):
// [1 4 7]
// [2 5 8]
// [3 6 9]
GLfloat matrix3x3[9] = {
1.0f, 2.0f, 3.0f, // First column
4.0f, 5.0f, 6.0f, // Second column
7.0f, 8.0f, 9.0f // Third column
};
// Expected values when transpose = GL_FALSE (no transpose):
// [1 4 7]
// [2 5 8]
// [3 6 9]
GLfloat expected3x3_no_transpose[9] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f};
// Expected values when transpose = GL_TRUE (transposed):
// [1 2 3]
// [4 5 6]
// [7 8 9]
// Stored in column-major order: [1, 4, 7, 2, 5, 8, 3, 6, 9]
GLfloat expected3x3_transpose[9] = {1.0f, 4.0f, 7.0f, 2.0f, 5.0f, 8.0f, 3.0f, 6.0f, 9.0f};
// Test with transpose = GL_FALSE
UniformMatrix3fv(locMat3, 1, GL_FALSE, matrix3x3);
GLfloat result3x3_no_transpose[9];
GetUniformfv(program, locMat3, result3x3_no_transpose);
for (int i = 0; i < 9; i++) {
EXPECT_FLOAT_EQ(result3x3_no_transpose[i], expected3x3_no_transpose[i]);
}
// Test with transpose = GL_TRUE
UniformMatrix3fv(locMat3, 1, GL_TRUE, matrix3x3);
GLfloat result3x3_transpose[9];
GetUniformfv(program, locMat3, result3x3_transpose);
for (int i = 0; i < 9; i++) {
EXPECT_FLOAT_EQ(result3x3_transpose[i], expected3x3_transpose[i]);
}
// Test 4x4 matrix transpose
auto locProjMat = GetUniformLocation(program, "ProjMat");
ASSERT_NE(locProjMat, -1);
// Test matrix (column-major as expected by OpenGL):
// [1 5 9 13]
// [2 6 10 14]
// [3 7 11 15]
// [4 8 12 16]
GLfloat matrix4x4[16] = {
1.0f, 2.0f, 3.0f, 4.0f, // First column
5.0f, 6.0f, 7.0f, 8.0f, // Second column
9.0f, 10.0f, 11.0f, 12.0f, // Third column
13.0f, 14.0f, 15.0f, 16.0f // Fourth column
};
// Expected values when transpose = GL_FALSE (no transpose):
// [1 5 9 13]
// [2 6 10 14]
// [3 7 11 15]
// [4 8 12 16]
GLfloat expected4x4_no_transpose[16] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f,
9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f};
// Expected values when transpose = GL_TRUE (transposed):
// [1 2 3 4]
// [5 6 7 8]
// [9 10 11 12]
// [13 14 15 16]
// Stored in column-major order
GLfloat expected4x4_transpose[16] = {1.0f, 5.0f, 9.0f, 13.0f, 2.0f, 6.0f, 10.0f, 14.0f,
3.0f, 7.0f, 11.0f, 15.0f, 4.0f, 8.0f, 12.0f, 16.0f};
// Test with transpose = GL_FALSE
UniformMatrix4fv(locProjMat, 1, GL_FALSE, matrix4x4);
GLfloat result4x4_no_transpose[16];
GetUniformfv(program, locProjMat, result4x4_no_transpose);
for (int i = 0; i < 16; i++) {
EXPECT_FLOAT_EQ(result4x4_no_transpose[i], expected4x4_no_transpose[i]);
}
// Test with transpose = GL_TRUE
UniformMatrix4fv(locProjMat, 1, GL_TRUE, matrix4x4);
GLfloat result4x4_transpose[16];
GetUniformfv(program, locProjMat, result4x4_transpose);
for (int i = 0; i < 16; i++) {
EXPECT_FLOAT_EQ(result4x4_transpose[i], expected4x4_transpose[i]);
}
}
TEST_F(ProgramTest, UniformLocationGaps) {
char infoLog[1024] = "";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSrc, NULL);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &fsSrc, NULL);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
BindAttribLocation(program, 1, "fIn1");
BindAttribLocation(program, 3, "fIn3");
BindAttribLocation(program, 5, "fIn5");
LinkProgram(program);
UseProgram(program);
int uniformCount = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &uniformCount);
ASSERT_LT(uniformCount, 4000);
// Test that uniform locations are correctly assigned even with gaps
// ProjMat is at location 0
ASSERT_EQ(GetUniformLocation(program, "ProjMat"), 0);
// TestMat3 is at location 10 (gap from 1-9)
ASSERT_EQ(GetUniformLocation(program, "TestMat3"), 10);
// TestMat2 is at location 20 (gap from 11-19)
ASSERT_EQ(GetUniformLocation(program, "TestMat2"), 20);
// Gray is at location 1 (no gap)
ASSERT_EQ(GetUniformLocation(program, "Gray"), 1);
// Saturation is at location 6 (gap from 2-5)
ASSERT_EQ(GetUniformLocation(program, "Saturation"), 6);
// Verify that locations in gaps correctly return -1
ASSERT_EQ(GetUniformLocation(program, "NonExistentUniform"), -1);
// Test uniform operations on locations with gaps
GLfloat matrix3[9] = {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
// Test setting and getting uniform at location 10 (TestMat3)
UniformMatrix3fv(10, 1, GL_FALSE, matrix3);
GLfloat result[9];
GetUniformfv(program, 10, result);
for (int i = 0; i < 9; i++) {
EXPECT_FLOAT_EQ(result[i], matrix3[i]);
}
// Test setting and getting uniform at location 20 (TestMat2)
GLfloat matrix2[4] = {1.0f, 0.0f, 0.0f, 1.0f};
UniformMatrix2fv(20, 1, GL_FALSE, matrix2);
GLfloat result2[4];
GetUniformfv(program, 20, result2);
for (int i = 0; i < 4; i++) {
EXPECT_FLOAT_EQ(result2[i], matrix2[i]);
}
// Test that accessing a gap location (e.g., 5) doesn't cause issues
// This should not crash or cause undefined behavior
Uniform1i(5, 114514); // Just to make sure we don't crash
// Verify that we can still use uniforms with sequential locations
auto locRed = GetUniformLocation(program, "RedMatrix");
Uniform3f(locRed, 1.0, 3.0, 5.0);
float redVal[3];
GetUniformfv(program, locRed, redVal);
ASSERT_EQ(redVal[0], 1.0);
ASSERT_EQ(redVal[1], 3.0);
ASSERT_EQ(redVal[2], 5.0);
}
const char* mc_position_tex_fs = R"(#version 150
uniform sampler2D Sampler0;
uniform vec4 ColorModulator;
in vec2 texCoord0;
out vec4 fragColor;
void main() {
vec4 color = texture(Sampler0, texCoord0);
if (color.a == 0.0) {
discard;
}
fragColor = color * ColorModulator;
}
)";
const char* mc_position_tex_vs = R"(#version 150
in vec3 Position;
in vec2 UV0;
uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
out vec2 texCoord0;
void main() {
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
texCoord0 = UV0;
}
)";
TEST_F(ProgramTest, MinecraftPositionTex) {
char infoLog[1024] = "";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &mc_position_tex_vs, NULL);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &mc_position_tex_fs, NULL);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
UseProgram(program);
int uniformCount = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &uniformCount);
ASSERT_LT(uniformCount, 4000);
int sampler0Loc = GetUniformLocation(program, "Sampler0");
ASSERT_GE(sampler0Loc, 0);
ASSERT_LT(sampler0Loc, 4000);
}
const char* minecraft_core_blit_screen_vs = R"(#version 150
in vec3 Position;
out vec2 texCoord;
void main() {
vec2 screenPos = Position.xy * 2.0 - 1.0;
gl_Position = vec4(screenPos.x, screenPos.y, 1.0, 1.0);
texCoord = Position.xy;
}
)";
const char* minecraft_core_lightmap = R"(#version 150
uniform float AmbientLightFactor;
uniform float SkyFactor;
uniform float BlockFactor;
uniform int UseBrightLightmap;
uniform vec3 SkyLightColor;
uniform float NightVisionFactor;
uniform float DarknessScale;
uniform float DarkenWorldFactor;
uniform float BrightnessFactor;
in vec2 texCoord;
out vec4 fragColor;
float get_brightness(float level) {
float curved_level = level / (4.0 - 3.0 * level);
return mix(curved_level, 1.0, AmbientLightFactor);
}
vec3 notGamma(vec3 x) {
vec3 nx = 1.0 - x;
return 1.0 - nx * nx * nx * nx;
}
void main() {
float block_brightness = get_brightness(floor(texCoord.x * 16) / 15) * BlockFactor;
float sky_brightness = get_brightness(floor(texCoord.y * 16) / 15) * SkyFactor;
// cubic nonsense, dips to yellowish in the middle, white when fully saturated
vec3 color = vec3(
block_brightness,
block_brightness * ((block_brightness * 0.6 + 0.4) * 0.6 + 0.4),
block_brightness * (block_brightness * block_brightness * 0.6 + 0.4)
);
if (UseBrightLightmap != 0) {
color = mix(color, vec3(0.99, 1.12, 1.0), 0.25);
color = clamp(color, 0.0, 1.0);
} else {
color += SkyLightColor * sky_brightness;
color = mix(color, vec3(0.75), 0.04);
vec3 darkened_color = color * vec3(0.7, 0.6, 0.6);
color = mix(color, darkened_color, DarkenWorldFactor);
}
if (NightVisionFactor > 0.0) {
// scale up uniformly until 1.0 is hit by one of the colors
float max_component = max(color.r, max(color.g, color.b));
if (max_component < 1.0) {
vec3 bright_color = color / max_component;
color = mix(color, bright_color, NightVisionFactor);
}
}
if (UseBrightLightmap == 0) {
color = clamp(color - vec3(DarknessScale), 0.0, 1.0);
}
vec3 notGamma = notGamma(color);
color = mix(color, notGamma, BrightnessFactor);
color = mix(color, vec3(0.75), 0.04);
color = clamp(color, 0.0, 1.0);
fragColor = vec4(color, 1.0);
}
)";
TEST_F(ProgramTest, MinecraftBlitScreenLightmap) {
char infoLog[1024] = "";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &minecraft_core_blit_screen_vs, NULL);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &minecraft_core_lightmap, NULL);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
UseProgram(program);
int uniformCount = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &uniformCount);
ASSERT_LT(uniformCount, 4000);
int loc = GetUniformLocation(program, "AmbientLightFactor");
ASSERT_GE(loc, 0);
ASSERT_LT(loc, 4000);
auto programObject = MG_State::pGLContext->GetCurrentProgram();
ASSERT_GT(programObject->GetUBOSize(), 0);
}
// const char* minecraft_core_tex_color_1216_vs = R"(#version 150
//
//// Can't moj_import in things used during startup, when resource packs don't exist.
//// This is a copy of dynamicimports.glsl and projection.glsl
// layout(std140) uniform DynamicTransforms {
// mat4 ModelViewMat;
// vec4 ColorModulator;
// vec3 ModelOffset;
// mat4 TextureMat;
// float LineWidth;
// };
// layout(std140) uniform Projection {
// mat4 ProjMat;
// };
//
// in vec3 Position;
// in vec2 UV0;
// in vec4 Color;
//
// out vec2 texCoord0;
// out vec4 vertexColor;
//
// void main() {
// gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
//
// texCoord0 = UV0;
// vertexColor = Color;
// }
//)";
//
// const char* minecraft_core_tex_color_1216_fs = R"(#version 150
//
//// Can't moj_import in things used during startup, when resource packs don't exist.
//// This is a copy of dynamicimports.glsl
// layout(std140) uniform DynamicTransforms {
// mat4 ModelViewMat;
// vec4 ColorModulator;
// vec3 ModelOffset;
// mat4 TextureMat;
// float LineWidth;
// };
//
// uniform sampler2D Sampler0;
//
// in vec2 texCoord0;
// in vec4 vertexColor;
//
// out vec4 fragColor;
//
// void main() {
// vec4 color = texture(Sampler0, texCoord0) * vertexColor;
// if (color.a == 0.0) {
// discard;
// }
// fragColor = color * ColorModulator;
// }
//)";
//
// TEST_F(ProgramTest, MinecraftTexColor1_21_6) {
// char infoLog[1024] = "";
//
// GLuint vs = CreateShader(GL_VERTEX_SHADER);
// ShaderSource(vs, 1, &minecraft_core_tex_color_1216_vs, NULL);
// CompileShader(vs);
// GLint vsStatus = GL_FALSE;
// GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
// GetShaderInfoLog(vs, 1024, nullptr, infoLog);
// ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
//
// GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
// ShaderSource(fs, 1, &minecraft_core_tex_color_1216_fs, NULL);
// CompileShader(fs);
// GLint fsStatus = GL_FALSE;
// GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
// GetShaderInfoLog(fs, 1024, nullptr, infoLog);
// ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
//
// GLuint program = CreateProgram();
// AttachShader(program, vs);
// AttachShader(program, fs);
//
// LinkProgram(program);
//
// UseProgram(program);
//
// int uniformCount = 0;
// GetProgramiv(program, GL_ACTIVE_UNIFORMS, &uniformCount);
// ASSERT_LT(uniformCount, 4000);
//
// auto transformuboIdx = GetUniformBlockIndex(program, "DynamicTransforms");
//
// auto programObject = MG_State::pGLContext->GetCurrentProgram();
// ASSERT_EQ(programObject->GetUBOSize(), 0);
//
// // auto& spirvs = programObject->GetGeneratedSpirv();
// // for (auto spirv: spirvs) {
// // MG_Util::ShaderTranspiler::SpvcSession spvcSession(spirv);
// // spvc_compiler_options options;
// // spvcSession.CreateOptions(&options);
// //
// // spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
// // spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
// // // spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_TRUE);
// //
// // spvcSession.SetOptions(options);
// //
// // const char* result = nullptr;
// // spvcSession.Compile(&result);
// // printf("%s\n\n", result);
// // }
// }
const char* optifine_vs1 = R"(#version 460 core
in vec3 Position;
in vec2 UV0;
uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
out vec2 texCoord0;
void main() {
gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0);
texCoord0 = UV0;
}
)";
const char* optifine_fs1 = R"(#version 460 core
uniform sampler2D Sampler0;
uniform vec4 ColorModulator;
in vec2 texCoord0;
out vec4 fragColor;
void main() {
vec4 color = texture(Sampler0, texCoord0);
if (color.a == 0.0) {
discard;
}
fragColor = color * ColorModulator;
})";
TEST_F(ProgramTest, CompileAndLinkWithExplicitVertexIn) {
char infoLog[1024] = "";
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &optifine_fs1, NULL);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &optifine_vs1, NULL);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint program = CreateProgram();
AttachShader(program, fs);
AttachShader(program, vs);
BindAttribLocation(program, 0, "Position");
BindAttribLocation(program, 2, "UV0");
BindAttribLocation(program, 1, "Color");
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE);
printf("Program linked.\n");
UseProgram(program);
GLint posLoc = GetAttribLocation(program, "Position");
ASSERT_EQ(posLoc, 0);
GLint uv0Loc = GetAttribLocation(program, "UV0");
ASSERT_EQ(uv0Loc, 2);
auto programObject = MG_State::pGLContext->GetCurrentProgram();
auto& spirvs = programObject->GetGeneratedSpirv();
// auto& vertexSpirv = spirvs[1]; // 0 - fragment, 1 - vertex
char* pSrcVertIn = nullptr;
const char* needle = "layout(location = 2) in vec2 UV0;";
for (auto spirv : spirvs) {
MG_Util::ShaderTranspiler::SpvcSession spvcSession(spirv, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
spvc_compiler_options options;
spvcSession.CreateOptions(&options);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 460);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_FALSE);
// spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
spvcSession.SetOptions(options);
const char* result = nullptr;
spvcSession.Compile(&result);
printf("%s\n\n", result);
const char* ret = strstr(result, needle);
if (ret) pSrcVertIn = (char*)ret;
}
ASSERT_TRUE(pSrcVertIn != nullptr) << "Not found expected string in generated shader.\n(Searching for \"" << needle
<< "\")";
}
TEST_F(ProgramTest, InactiveExplicitVertexBindingsDoNotReserveLocations) {
const char* vertexSource = R"(#version 430 compatibility
in vec3 Position;
in vec2 UV0;
in vec3 vaPosition;
void main() {
gl_Position = vec4(vaPosition, 1.0);
}
)";
const char* fragmentSource = R"(#version 430 compatibility
out vec4 fragColor;
void main() {
fragColor = vec4(1.0);
}
)";
GLuint vertexShader = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vertexShader, 1, &vertexSource, nullptr);
CompileShader(vertexShader);
GLint compileStatus = GL_FALSE;
GetShaderiv(vertexShader, GL_COMPILE_STATUS, &compileStatus);
ASSERT_EQ(compileStatus, GL_TRUE);
GLuint fragmentShader = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fragmentShader, 1, &fragmentSource, nullptr);
CompileShader(fragmentShader);
GetShaderiv(fragmentShader, GL_COMPILE_STATUS, &compileStatus);
ASSERT_EQ(compileStatus, GL_TRUE);
GLuint program = CreateProgram();
AttachShader(program, vertexShader);
AttachShader(program, fragmentShader);
// Iris binds these canonical names before linking every program. Its compatibility
// transformer can inject both declarations even when the shader pack instead reads
// vaPosition. Inactive API bindings must not consume locations during the link.
BindAttribLocation(program, 0, "Position");
BindAttribLocation(program, 1, "UV0");
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE);
EXPECT_EQ(GetAttribLocation(program, "Position"), -1);
EXPECT_EQ(GetAttribLocation(program, "UV0"), -1);
EXPECT_EQ(GetAttribLocation(program, "vaPosition"), 0);
auto programObject = MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
const Int vertexIndex = programObject->GetShaderIndexByStage(ShaderStage::Vertex);
ASSERT_GE(vertexIndex, 0);
const auto& spirvs = programObject->GetGeneratedSpirv();
ASSERT_LT(static_cast<SizeT>(vertexIndex), spirvs.size());
const auto& vertexSpirv = spirvs[vertexIndex];
spv_reflect::ShaderModule reflection(vertexSpirv.size() * sizeof(Uint), vertexSpirv.data());
ASSERT_EQ(reflection.GetResult(), SPV_REFLECT_RESULT_SUCCESS);
uint32_t inputCount = 0;
ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, nullptr), SPV_REFLECT_RESULT_SUCCESS);
Vector<SpvReflectInterfaceVariable*> inputs(inputCount);
ASSERT_EQ(reflection.EnumerateInputVariables(&inputCount, inputs.data()), SPV_REFLECT_RESULT_SUCCESS);
Uint32 userInputCount = 0;
Uint32 locationMask = 0;
for (const auto* input : inputs) {
if (input == nullptr || (input->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0) {
continue;
}
ASSERT_LT(input->location, 32u);
locationMask |= 1u << input->location;
++userInputCount;
}
EXPECT_EQ(userInputCount, 1u);
EXPECT_EQ(locationMask, 0x1u);
}
TEST_F(ProgramTest, CompileAndLinkWithExplicitFragmentOut) {
char infoLog[1024] = "";
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &optifine_fs1, NULL);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &optifine_vs1, NULL);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint program = CreateProgram();
AttachShader(program, fs);
AttachShader(program, vs);
BindFragDataLocation(program, 7, "fragColor");
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE);
printf("Program linked.\n");
UseProgram(program);
GLint fragColorLoc = GetFragDataLocation(program, "fragColor");
ASSERT_EQ(fragColorLoc, 7);
// glGetFragDataIndex: a valid user output uses color index 0 (dual-source index 1 is not tracked);
// a name that is not an active output returns -1. Neither records a GL error.
EXPECT_EQ(GetFragDataIndex(program, "fragColor"), 0);
EXPECT_EQ(GetFragDataIndex(program, "notAnActiveOutput"), -1);
auto programObject = MG_State::pGLContext->GetCurrentProgram();
auto& spirvs = programObject->GetGeneratedSpirv();
auto& fragSpirv = spirvs[programObject->GetShaderIndexByStage(ShaderStage::Fragment)];
char* pSrcfragOut = nullptr;
const char* needle = "layout(location = 7) out vec4 fragColor;";
// for (auto spirv: spirvs) {
MG_Util::ShaderTranspiler::SpvcSession spvcSession(fragSpirv, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
spvc_compiler_options options;
spvcSession.CreateOptions(&options);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 460);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_FALSE);
// spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
spvcSession.SetOptions(options);
const char* result = nullptr;
spvcSession.Compile(&result);
printf("%s\n\n", result);
const char* ret = strstr(result, needle);
if (ret) pSrcfragOut = (char*)ret;
// }
ASSERT_TRUE(pSrcfragOut != nullptr) << "Not found expected string in generated shader.\n(Searching for \"" << needle
<< "\")";
// glBindFragDataLocationIndexed round-trips the color index through a re-link. index 1 requires
// colorNumber 0 (GL_MAX_DUAL_SOURCE_DRAW_BUFFERS is 1).
BindFragDataLocationIndexed(program, 0, 1, "fragColor");
LinkProgram(program);
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE);
EXPECT_EQ(GetFragDataIndex(program, "fragColor"), 1);
EXPECT_EQ(GetFragDataIndex(program, "notAnActiveOutput"), -1);
// The color index must reach the transpiled shader as layout(location = 0, index = 1) so the
// driver binds fragColor as the second dual-source input; the SPIR-V Index decoration set from
// the glslang layoutIndex round-trips through SPIRV-Cross.
auto& spirvsIndexed = programObject->GetGeneratedSpirv();
auto& fragSpirvIndexed = spirvsIndexed[programObject->GetShaderIndexByStage(ShaderStage::Fragment)];
MG_Util::ShaderTranspiler::SpvcSession spvcSessionIndexed(fragSpirvIndexed,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
spvc_compiler_options optionsIndexed;
spvcSessionIndexed.CreateOptions(&optionsIndexed);
spvc_compiler_options_set_uint(optionsIndexed, SPVC_COMPILER_OPTION_GLSL_VERSION, 460);
spvc_compiler_options_set_bool(optionsIndexed, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_FALSE);
spvcSessionIndexed.SetOptions(optionsIndexed);
const char* resultIndexed = nullptr;
spvcSessionIndexed.Compile(&resultIndexed);
printf("%s\n\n", resultIndexed);
const char* indexNeedle = "index = 1";
ASSERT_TRUE(strstr(resultIndexed, indexNeedle) != nullptr)
<< "Expected dual-source color index in generated shader.\n(Searching for \"" << indexNeedle << "\")";
// glBindFragDataLocation is equivalent to index 0 and resets it.
BindFragDataLocation(program, 0, "fragColor");
LinkProgram(program);
EXPECT_EQ(GetFragDataIndex(program, "fragColor"), 0);
// Validation: index > 1 and a too-large colorNumber for index 1 are GL_INVALID_VALUE; a gl_ name is
// GL_INVALID_OPERATION.
BindFragDataLocationIndexed(program, 0, 2, "fragColor");
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
BindFragDataLocationIndexed(program, 1, 1, "fragColor"); // colorNumber 1 invalid for index 1
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
BindFragDataLocationIndexed(program, 0, 0, "gl_FragColor");
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
const char* vs_sampler_as_varname = R"(#version 330
in vec3 Position;
out float sphericalVertexDistance;
out float cylindricalVertexDistance;
out vec4 vertexColor;
out vec2 texCoord0;
void main() {
gl_Position = vec4(Position, 1.0);
sphericalVertexDistance = 1.0;
cylindricalVertexDistance = 1.0;
vertexColor = vec4(Position, 1.0);
texCoord0 = Position.xy;
}
)";
const char* fs_sampler_as_varname = R"(#version 330
layout(std140) uniform Fog {
vec4 FogColor;
float FogEnvironmentalStart;
float FogEnvironmentalEnd;
float FogRenderDistanceStart;
float FogRenderDistanceEnd;
float FogSkyEnd;
float FogCloudsEnd;
};
float linear_fog_value(float vertexDistance, float fogStart, float fogEnd) {
if (vertexDistance <= fogStart) {
return 0.0;
} else if (vertexDistance >= fogEnd) {
return 1.0;
}
return (vertexDistance - fogStart) / (fogEnd - fogStart);
}
float total_fog_value(float sphericalVertexDistance, float cylindricalVertexDistance, float environmentalStart, float environmantalEnd, float renderDistanceStart, float renderDistanceEnd) {
return max(linear_fog_value(sphericalVertexDistance, environmentalStart, environmantalEnd), linear_fog_value(cylindricalVertexDistance, renderDistanceStart, renderDistanceEnd));
}
vec4 apply_fog(vec4 inColor, float sphericalVertexDistance, float cylindricalVertexDistance, float environmentalStart, float environmantalEnd, float renderDistanceStart, float renderDistanceEnd, vec4 fogColor) {
float fogValue = total_fog_value(sphericalVertexDistance, cylindricalVertexDistance, environmentalStart, environmantalEnd, renderDistanceStart, renderDistanceEnd);
return vec4(mix(inColor.rgb, fogColor.rgb, fogValue * fogColor.a), inColor.a);
}
float fog_spherical_distance(vec3 pos) {
return length(pos);
}
float fog_cylindrical_distance(vec3 pos) {
float distXZ = length(pos.xz);
float distY = abs(pos.y);
return max(distXZ, distY);
}
uniform float fTime;
layout(std140) uniform Globals {
ivec3 CameraBlockPos;
vec3 CameraOffset;
vec2 ScreenSize;
float GlintAlpha;
float GameTime;
int MenuBlurRadius;
int UseRgss;
};
layout(std140) uniform ChunkSection {
mat4 ModelViewMat;
float ChunkVisibility;
ivec2 TextureSize;
ivec3 ChunkPosition;
};
uniform sampler2D Sampler0;
in float sphericalVertexDistance;
in float cylindricalVertexDistance;
in vec4 vertexColor;
in vec2 texCoord0;
out vec4 fragColor;
vec4 sampleNearest(sampler2D sampler, vec2 uv, vec2 pixelSize, vec2 du, vec2 dv, vec2 texelScreenSize) {
// Convert our UV back up to texel coordinates and find out how far over we are from the center of each pixel
vec2 uvTexelCoords = uv / pixelSize;
vec2 texelCenter = round(uvTexelCoords) - 0.5f;
vec2 texelOffset = uvTexelCoords - texelCenter;
// Move our offset closer to the texel center based on texel size on screen
texelOffset = (texelOffset - 0.5f) * pixelSize / texelScreenSize + 0.5f;
texelOffset = clamp(texelOffset, 0.0f, 1.0f);
uv = (texelCenter + texelOffset) * pixelSize;
return textureGrad(sampler, uv, du, dv);
}
vec4 sampleNearest(sampler2D source, vec2 uv, vec2 pixelSize) {
vec2 du = dFdx(uv);
vec2 dv = dFdy(uv);
vec2 texelScreenSize = sqrt(du * du + dv * dv);
return sampleNearest(source, uv, pixelSize, du, dv, texelScreenSize);
}
// Rotated Grid Super-Sampling
vec4 sampleRGSS(sampler2D source, vec2 uv, vec2 pixelSize) {
vec2 du = dFdx(uv);
vec2 dv = dFdy(uv);
vec2 texelScreenSize = sqrt(du * du + dv * dv);
float maxTexelSize = max(texelScreenSize.x, texelScreenSize.y);
float minPixelSize = min(pixelSize.x, pixelSize.y);
float transitionStart = minPixelSize * 1.0;
float transitionEnd = minPixelSize * 2.0;
float blendFactor = smoothstep(transitionStart, transitionEnd, maxTexelSize);
float duLength = length(du);
float dvLength = length(dv);
float minDerivative = min(duLength, dvLength);
float maxDerivative = max(duLength, dvLength);
float effectiveDerivative = sqrt(minDerivative * maxDerivative);
float mipLevelExact = max(0.0, log2(effectiveDerivative / minPixelSize));
float mipLevelLow = floor(mipLevelExact);
float mipLevelHigh = mipLevelLow + 1.0;
float mipBlend = fract(mipLevelExact);
const vec2 offsets[4] = vec2[](
vec2(0.125, 0.375),
vec2(-0.125, -0.375),
vec2(0.375, -0.125),
vec2(-0.375, 0.125)
);
vec4 rgssColorLow = vec4(0.0);
vec4 rgssColorHigh = vec4(0.0);
for (int i = 0; i < 4; ++i) {
vec2 sampleUV = uv + offsets[i] * pixelSize;
rgssColorLow += textureLod(source, sampleUV, mipLevelLow);
rgssColorHigh += textureLod(source, sampleUV, mipLevelHigh);
}
rgssColorLow *= 0.25;
rgssColorHigh *= 0.25;
vec4 rgssColor = mix(rgssColorLow, rgssColorHigh, mipBlend);
vec4 nearestColor = sampleNearest(source, uv, pixelSize, du, dv, texelScreenSize);
return mix(nearestColor, rgssColor, blendFactor);
}
void main() {
vec4 color = (UseRgss == 1 ? sampleRGSS(Sampler0, texCoord0, 1.0f / TextureSize) : sampleNearest(Sampler0, texCoord0, 1.0f / TextureSize)) * vertexColor;
color = mix(FogColor * vec4(1, 1, 1, color.a * fTime), color, ChunkVisibility);
#ifdef ALPHA_CUTOUT
if (color.a < ALPHA_CUTOUT) {
discard;
}
#endif
fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, FogEnvironmentalStart, FogEnvironmentalEnd, FogRenderDistanceStart, FogRenderDistanceEnd, FogColor);
})";
TEST_F(ProgramTest, GetFragDataIndexRejectsInvalidProgram) {
// A handle that was never generated is rejected and returns -1. Like glGetFragDataLocation, this
// routes through the shared program-name check, which records GL_INVALID_VALUE for an unknown name.
EXPECT_EQ(GetFragDataIndex(999999u, "fragColor"), -1);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
// Exactly ONE error is recorded per bad call: the redundant second GL_INVALID_OPERATION that the
// FragData entry points used to queue on top of the name check has been removed.
EXPECT_EQ(GetError(), GL_NO_ERROR);
// Defensive drain: keep the shared error queue clean regardless (the fixture never resets it).
while (GetError() != GL_NO_ERROR) {}
}
TEST_F(ProgramTest, CompileShaderWithSamplerAsVarName) {
char infoLog[1024] = "";
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &fs_sampler_as_varname, NULL);
CompileShader(fs);
GLint fsStatus = GL_FALSE;
GetShaderiv(fs, GL_COMPILE_STATUS, &fsStatus);
GetShaderInfoLog(fs, 1024, nullptr, infoLog);
ASSERT_EQ(fsStatus, GL_TRUE) << infoLog;
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vs_sampler_as_varname, NULL);
CompileShader(vs);
GLint vsStatus = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &vsStatus);
GetShaderInfoLog(vs, 1024, nullptr, infoLog);
ASSERT_EQ(vsStatus, GL_TRUE) << infoLog;
GLuint program = CreateProgram();
AttachShader(program, fs);
AttachShader(program, vs);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE);
printf("Program linked.\n");
UseProgram(program);
auto programObject = MG_State::pGLContext->GetCurrentProgram();
auto& spirvs = programObject->GetGeneratedSpirv();
auto& fragSpirv = spirvs[programObject->GetShaderIndexByStage(ShaderStage::Fragment)];
MG_Util::ShaderTranspiler::SpvcSession spvcSession(fragSpirv, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
spvc_compiler_options options;
spvcSession.CreateOptions(&options);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
spvcSession.SetOptions(options);
const char* result = nullptr;
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<GLint>(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<GLuint>(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);
}
namespace {
GLuint LinkVsFsProgram(const char* vsSource, const char* fsSource) {
char infoLog[4096] = "";
GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSource, 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, &fsSource, 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;
}
const char* kPassthroughCoordsVs = R"(#version 330
in vec4 a_position;
in vec4 a_coords;
out vec4 coords_in;
void main() {
gl_Position = a_position;
coords_in = a_coords;
})";
} // namespace
// Repro for KHR-GL33.shaders.loops.do_while_dynamic_iterations.empty_body_* (and the
// only_continue / unconditional_break variants): the loop is dead code, so the SPIR-V
// optimizer eliminates it together with the only loads of `one` / `ui_one` -- and with
// them the entire global UBO. The uniforms stay active in link reflection, so
// glUniform1i on them must still have backing storage instead of memcpy-ing to null.
TEST_F(ProgramTest, DoWhileDeadLoopUniformsKeepBackingStorage) {
const char* loopBodies[] = {"", "continue;", "break;"};
for (const char* body : loopBodies) {
const String fsSource = String(R"(#version 330
uniform int ui_one;
uniform mediump int one;
in vec4 coords_in;
out vec4 o_color;
void main() {
vec4 res = coords_in;
mediump int i = 0;
do {)") + body + R"(} while (i++ < one*ui_one);
o_color = res;
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource.c_str());
const GLint locOne = GetUniformLocation(program, "one");
const GLint locUiOne = GetUniformLocation(program, "ui_one");
ASSERT_GE(locOne, 0) << "body: '" << body << "'";
ASSERT_GE(locUiOne, 0) << "body: '" << body << "'";
UseProgram(program);
Uniform1i(locOne, 1); // crashed with a null MapUBO() before the fallback storage
Uniform1i(locUiOne, 2);
EXPECT_EQ(GetError(), GL_NO_ERROR) << "body: '" << body << "'";
GLint readback = -1;
GetUniformiv(program, locOne, &readback);
EXPECT_EQ(readback, 1) << "body: '" << body << "'";
readback = -1;
GetUniformiv(program, locUiOne, &readback);
EXPECT_EQ(readback, 2) << "body: '" << body << "'";
EXPECT_EQ(GetError(), GL_NO_ERROR) << "body: '" << body << "'";
}
}
// Repro for KHR-GL33.shaders.struct.uniform.*nested_struct_array_*: leaf uniforms of
// nested struct arrays need (a) one location per array element and (b) real byte
// offsets inside the global UBO. Before the fix every leaf had a single location and
// offset 0, so glUniform2fv(loc, 2, ...) tripped the size assert on the neighboring
// float uniform (and corrupted it in release builds).
TEST_F(ProgramTest, NestedStructArrayUniformElementWrites) {
// Struct shape from CTS glcShaderStructTests nested_struct_array (uniform case).
const char* fsSource = R"(#version 330
struct T {
mediump float a;
mediump vec2 b[2];
};
struct S {
mediump float a;
T b[3];
int c;
};
uniform S s[2];
in vec4 coords_in;
out vec4 o_color;
void main() {
mediump float r = (s[0].b[1].b[0].x + s[1].b[2].b[1].y) * s[0].b[0].a;
mediump float g = s[1].b[0].b[0].y * s[0].b[2].a * s[1].b[2].a;
mediump float b = (s[0].b[2].b[1].y + s[0].b[1].b[0].y + s[1].a) * s[0].b[1].a;
mediump float a = float(s[0].c) + s[1].b[2].a - s[1].b[1].a;
o_color = vec4(r, g, b, a);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
UseProgram(program);
const GLint locVecArray = GetUniformLocation(program, "s[0].b[1].b");
ASSERT_GE(locVecArray, 0);
// Element locations are consecutive and reachable via the "[k]" suffix.
EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[0]"), locVecArray);
EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[1]"), locVecArray + 1);
EXPECT_EQ(GetUniformLocation(program, "s[0].b[1].b[2]"), -1);
// Distinct scalar leaves must land at distinct UBO offsets (they all aliased
// offset 0 before the fix).
const char* scalarLeaves[] = {"s[0].b[0].a", "s[0].b[1].a", "s[0].b[2].a", "s[1].a", "s[1].b[1].a",
"s[1].b[2].a"};
const GLfloat scalarValues[] = {0.5f, 0.25f, 0.125f, 7.0f, 3.0f, 4.0f};
for (SizeT i = 0; i < std::size(scalarLeaves); ++i) {
const GLint loc = GetUniformLocation(program, scalarLeaves[i]);
ASSERT_GE(loc, 0) << scalarLeaves[i];
Uniform1f(loc, scalarValues[i]);
}
// CTS-style whole-array write: glUniform2fv with count = 2 on a vec2[2] leaf.
// Before the fix this asserted/corrupted the next uniform ("s[0].b[2].a").
const GLfloat vecData[4] = {1.0f, 2.0f, 3.0f, 4.0f};
Uniform2fv(locVecArray, 2, vecData);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLfloat vecReadback[2] = {};
GetUniformfv(program, locVecArray, vecReadback);
EXPECT_EQ(vecReadback[0], 1.0f);
EXPECT_EQ(vecReadback[1], 2.0f);
GetUniformfv(program, locVecArray + 1, vecReadback);
EXPECT_EQ(vecReadback[0], 3.0f);
EXPECT_EQ(vecReadback[1], 4.0f);
// All scalar leaves survived the array write intact.
for (SizeT i = 0; i < std::size(scalarLeaves); ++i) {
GLfloat readback = -1.0f;
GetUniformfv(program, GetUniformLocation(program, scalarLeaves[i]), &readback);
EXPECT_EQ(readback, scalarValues[i]) << scalarLeaves[i];
}
// std140: vec2 array elements inside the struct are 16 bytes apart, and the
// per-element offsets differ.
auto programObject = MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
const Uint offsetElement0 = programObject->GetUniformOffset(static_cast<Uint>(locVecArray));
const Uint offsetElement1 = programObject->GetUniformOffset(static_cast<Uint>(locVecArray + 1));
EXPECT_EQ(offsetElement1, offsetElement0 + 16u);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Plain top-level uniform arrays share the same per-element location machinery.
TEST_F(ProgramTest, PlainArrayUniformElementLocationsAndWrites) {
const char* fsSource = R"(#version 330
uniform float arr[4];
uniform float guard;
in vec4 coords_in;
out vec4 o_color;
void main() {
o_color = vec4(arr[0] + arr[1], arr[2] + arr[3], guard, 1.0);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
UseProgram(program);
const GLint locArr = GetUniformLocation(program, "arr");
ASSERT_GE(locArr, 0);
EXPECT_EQ(GetUniformLocation(program, "arr[0]"), locArr);
EXPECT_EQ(GetUniformLocation(program, "arr[2]"), locArr + 2);
EXPECT_EQ(GetUniformLocation(program, "arr[4]"), -1);
const GLint locGuard = GetUniformLocation(program, "guard");
ASSERT_GE(locGuard, 0);
EXPECT_EQ(GetUniformLocation(program, "guard[0]"), -1); // not an array
Uniform1f(locGuard, 9.0f);
const GLfloat values[4] = {1.0f, 2.0f, 3.0f, 4.0f};
Uniform1fv(locArr, 4, values);
for (int i = 0; i < 4; ++i) {
GLfloat readback = -1.0f;
GetUniformfv(program, locArr + i, &readback);
EXPECT_EQ(readback, values[i]) << "arr[" << i << "]";
}
// Overlong writes stop at the end of the array (GL 3.3 §2.11.4) instead of
// spilling into the next uniform.
const GLfloat tail[3] = {30.0f, 40.0f, 50.0f};
Uniform1fv(GetUniformLocation(program, "arr[2]"), 3, tail);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLfloat readback = -1.0f;
GetUniformfv(program, locArr + 2, &readback);
EXPECT_EQ(readback, 30.0f);
GetUniformfv(program, locArr + 3, &readback);
EXPECT_EQ(readback, 40.0f);
GetUniformfv(program, locGuard, &readback);
EXPECT_EQ(readback, 9.0f); // untouched by the overlong write
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------
// GL CTS KHR-GL33.shaders.uniform_block regression pack. MobileGL's SPIR-V
// pipeline lays every uniform block out as std140; the frontend implements the
// GL-visible consequences of that choice: packed/shared qualifiers compile (as
// std140), reflection uses GL naming ("arr[0]", per-element struct arrays),
// unused block members stay active, block sizes are vec4-padded, and array
// strides are std140 even for arrays nested inside struct members.
// ---------------------------------------------------------------------------
TEST_F(ProgramTest, UniformBlockPackedAndSharedLayoutsCompileAsStd140) {
const char* fsSource = R"(#version 330
layout(packed) uniform PackedBlock {
vec4 pv;
};
layout(shared, row_major) uniform SharedBlock {
float sf;
mat4 sm;
};
out vec4 o_color;
void main() {
o_color = pv + vec4(sf) + vec4(sm[0][0]);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
// The blocks land on the implementation's chosen layout: std140 offsets.
const GLuint pv = UniformIndexByName(program, "pv");
const GLuint sf = UniformIndexByName(program, "sf");
const GLuint sm = UniformIndexByName(program, "sm");
ASSERT_NE(pv, GL_INVALID_INDEX);
ASSERT_NE(sf, GL_INVALID_INDEX);
ASSERT_NE(sm, GL_INVALID_INDEX);
EXPECT_EQ(QueryUniformiv(program, pv, GL_UNIFORM_OFFSET), 0);
EXPECT_EQ(QueryUniformiv(program, sf, GL_UNIFORM_OFFSET), 0);
EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_OFFSET), 16);
// The remaining qualifiers in the rewritten layout() list survive.
EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_IS_ROW_MAJOR), 1);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, UniformBlockReflectsUnusedMembersWithGLNamesAndPaddedSize) {
const char* fsSource = R"(#version 330
layout(std140) uniform Blk {
float used;
vec4 unusedArr[3];
ivec3 tail;
};
out vec4 o_color;
void main() {
o_color = vec4(used);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
const GLuint blockIndex = GetUniformBlockIndex(program, "Blk");
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
// All three members are active (unusedArr and tail are never read), the array is
// reported under its GL name "unusedArr[0]", and both spellings resolve.
const GLuint used = UniformIndexByName(program, "used");
const GLuint unusedSuffixed = UniformIndexByName(program, "unusedArr[0]");
const GLuint unusedBare = UniformIndexByName(program, "unusedArr");
const GLuint tail = UniformIndexByName(program, "tail");
ASSERT_NE(used, GL_INVALID_INDEX);
ASSERT_NE(unusedSuffixed, GL_INVALID_INDEX);
ASSERT_NE(tail, GL_INVALID_INDEX);
EXPECT_EQ(unusedSuffixed, unusedBare);
char nameBuf[64] = "";
GLsizei nameLen = 0;
GLint arraySize = 0;
GLenum type = 0;
GetActiveUniform(program, unusedSuffixed, sizeof(nameBuf), &nameLen, &arraySize, &type, nameBuf);
EXPECT_STREQ(nameBuf, "unusedArr[0]");
EXPECT_EQ(arraySize, 3);
EXPECT_EQ(type, static_cast<GLenum>(GL_FLOAT_VEC4));
// std140 layout of the unused members.
EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_OFFSET), 16);
EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_ARRAY_STRIDE), 16);
EXPECT_EQ(QueryUniformiv(program, tail, GL_UNIFORM_OFFSET), 64);
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS agrees with the INDICES list and counts all members.
GLint activeInBlock = 0;
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &activeInBlock);
ASSERT_EQ(activeInBlock, 3);
GLint indices[3] = {-1, -1, -1};
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, indices);
for (GLint index : indices) {
EXPECT_TRUE(index == static_cast<GLint>(used) || index == static_cast<GLint>(unusedSuffixed) ||
index == static_cast<GLint>(tail));
}
// The block ends with an ivec3 at offset 64 (unpadded end 76); the backend compiles
// the std140 block at its vec4-padded size, and the reported size must cover it or
// buffers sized from this query are too small to draw with.
GLint dataSize = 0;
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
EXPECT_EQ(dataSize, 80);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, UniformBlockStructArrayExpandsPerElementWithStd140Strides) {
const char* fsSource = R"(#version 330
struct S {
ivec2 v[2];
float f;
};
layout(std140) uniform Blk2 {
S s[2];
} inst;
out vec4 o_color;
void main() {
o_color = vec4(inst.s[0].f);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
// ARB_program_interface_query naming: one entry per struct array element, prefixed
// with the BLOCK name (not the instance name), basic arrays suffixed with "[0]".
const GLuint v0 = UniformIndexByName(program, "Blk2.s[0].v[0]");
const GLuint f0 = UniformIndexByName(program, "Blk2.s[0].f");
const GLuint v1 = UniformIndexByName(program, "Blk2.s[1].v[0]");
const GLuint f1 = UniformIndexByName(program, "Blk2.s[1].f");
ASSERT_NE(v0, GL_INVALID_INDEX);
ASSERT_NE(f0, GL_INVALID_INDEX);
ASSERT_NE(v1, GL_INVALID_INDEX);
ASSERT_NE(f1, GL_INVALID_INDEX);
// std140: ivec2 v[2] rounds each element up to a vec4 (stride 16, NOT the tight 8
// glslang reflects for arrays nested inside a struct member); struct size rounds to
// 48, giving s[1] members a 48-byte bias.
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_OFFSET), 0);
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_ARRAY_STRIDE), 16);
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_SIZE), 2);
EXPECT_EQ(QueryUniformiv(program, f0, GL_UNIFORM_OFFSET), 32);
EXPECT_EQ(QueryUniformiv(program, v1, GL_UNIFORM_OFFSET), 48);
EXPECT_EQ(QueryUniformiv(program, f1, GL_UNIFORM_OFFSET), 80);
GLint dataSize = 0;
const GLuint blockIndex = GetUniformBlockIndex(program, "Blk2");
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
EXPECT_EQ(dataSize, 96);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, UniformBlockInstanceArrayReportsPerInstanceBlocks) {
const char* fsSource = R"(#version 330
layout(std140) uniform ArrBlk {
vec4 av;
} insts[2];
out vec4 o_color;
void main() {
o_color = insts[0].av + insts[1].av;
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
const GLuint inst0 = GetUniformBlockIndex(program, "ArrBlk[0]");
const GLuint inst1 = GetUniformBlockIndex(program, "ArrBlk[1]");
ASSERT_NE(inst0, GL_INVALID_INDEX);
ASSERT_NE(inst1, GL_INVALID_INDEX);
EXPECT_NE(inst0, inst1);
// A bare block name resolves to the first instance.
EXPECT_EQ(GetUniformBlockIndex(program, "ArrBlk"), inst0);
// Every instance of the array shares the single reflected member set.
GLint count0 = 0;
GLint count1 = 0;
GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count0);
GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count1);
EXPECT_EQ(count0, 1);
EXPECT_EQ(count1, 1);
GLint index0 = -1;
GLint index1 = -1;
GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index0);
GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index1);
EXPECT_EQ(index0, index1);
EXPECT_EQ(static_cast<GLuint>(index0), UniformIndexByName(program, "ArrBlk.av"));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, DeleteShaderWhileAttachedKeepsNameUsableUntilDetach) {
// GL CTS compiles through exactly this sequence (create, attach, DELETE, source,
// compile): glDeleteShader on an attached shader only flags it, and the name must
// keep working until the last detach.
const char* vsSource = R"(#version 330
void main() { gl_Position = vec4(0.0); }
)";
const char* fsSource = R"(#version 330
out vec4 o_color;
void main() { o_color = vec4(1.0); }
)";
GLuint program = CreateProgram();
GLuint vs = CreateShader(GL_VERTEX_SHADER);
AttachShader(program, vs);
DeleteShader(vs);
EXPECT_EQ(IsShader(vs), GL_TRUE); // still alive: attached
ShaderSource(vs, 1, &vsSource, nullptr);
CompileShader(vs);
GLint status = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &status);
EXPECT_EQ(status, GL_TRUE);
status = GL_FALSE;
GetShaderiv(vs, GL_DELETE_STATUS, &status);
EXPECT_EQ(status, GL_TRUE);
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
AttachShader(program, fs);
DeleteShader(fs);
ShaderSource(fs, 1, &fsSource, nullptr);
CompileShader(fs);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
char infoLog[1024] = "";
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(linkStatus, GL_TRUE) << infoLog;
// The last GL-visible detach releases the flagged shader's name.
DetachShader(program, vs);
EXPECT_EQ(IsShader(vs), GL_FALSE);
// Deleting the program releases the other flagged shader.
DeleteProgram(program);
EXPECT_EQ(IsShader(fs), GL_FALSE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---- P0a single-parse regression tests ----
// glCompileShader now performs the one link-compatible (relaxed Vulkan-rules) parse;
// these pin the GL frontend semantics that parse cannot provide by itself.
namespace {
GLuint CompileShaderChecked(GLenum type, const char* source) {
char infoLog[1024] = "";
GLuint shader = CreateShader(type);
ShaderSource(shader, 1, &source, nullptr);
CompileShader(shader);
GLint status = GL_FALSE;
GetShaderiv(shader, GL_COMPILE_STATUS, &status);
GetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(status, GL_TRUE) << infoLog;
return shader;
}
GLuint LinkVsFs(GLuint vs, GLuint fs, GLint expectedLinkStatus) {
char infoLog[2048] = "";
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, expectedLinkStatus) << infoLog;
return program;
}
} // namespace
// The relaxed parse sweeps every DECLARED default-block uniform into MGL_GLOBAL_UBO,
// including ones no stage reads. GL requires those to be inactive: absent from the
// glGetActiveUniform enumeration and -1 from glGetUniformLocation. The synthesized
// MGL_GLOBAL_UBO itself must not surface as a GL uniform block either.
TEST_F(ProgramTest, DeclaredButUnreadUniformIsInactiveAndGlobalUboStaysHidden) {
const char* vsSource = R"(#version 330 core
uniform mat4 uUsedMat;
uniform vec4 uDeadVec;
void main() { gl_Position = uUsedMat * vec4(1.0); }
)";
const char* fsSource = R"(#version 330 core
uniform vec4 uUsedColor;
uniform float uDeadFloat;
out vec4 fragColor;
void main() { fragColor = uUsedColor; }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
GLint activeUniforms = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeUniforms);
EXPECT_EQ(activeUniforms, 2);
EXPECT_NE(GetUniformLocation(program, "uUsedMat"), -1);
EXPECT_NE(GetUniformLocation(program, "uUsedColor"), -1);
EXPECT_EQ(GetUniformLocation(program, "uDeadVec"), -1);
EXPECT_EQ(GetUniformLocation(program, "uDeadFloat"), -1);
EXPECT_EQ(UniformIndexByName(program, "uDeadVec"), GL_INVALID_INDEX);
char nameBuf[64] = "";
for (GLint i = 0; i < activeUniforms; ++i) {
GLsizei nameLen = 0;
GLint size = 0;
GLenum type = 0;
GetActiveUniform(program, static_cast<GLuint>(i), sizeof(nameBuf), &nameLen, &size, &type, nameBuf);
EXPECT_TRUE(std::strcmp(nameBuf, "uDeadVec") != 0 && std::strcmp(nameBuf, "uDeadFloat") != 0)
<< nameBuf;
}
// No named blocks are declared, so GL must see zero uniform blocks - the global
// UBO the transpiler materializes is an implementation artifact.
GLint activeBlocks = 0;
GetProgramiv(program, GL_ACTIVE_UNIFORM_BLOCKS, &activeBlocks);
EXPECT_EQ(activeBlocks, 0);
EXPECT_EQ(GetUniformBlockIndex(program, "MGL_GLOBAL_UBO"), GL_INVALID_INDEX);
// Default-block uniforms report block index -1 and offset -1 even though the
// relaxed parse physically placed them in the global UBO.
const GLuint usedMat = UniformIndexByName(program, "uUsedMat");
ASSERT_NE(usedMat, GL_INVALID_INDEX);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_BLOCK_INDEX), -1);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_OFFSET), -1);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_ARRAY_STRIDE), -1);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_MATRIX_STRIDE), -1);
EXPECT_EQ(QueryUniformiv(program, usedMat, GL_UNIFORM_IS_ROW_MAJOR), 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Distinct uniforms whose explicit locations overlap across stages must fail the
// link (ARB_explicit_uniform_location). The GL-client parse used to reject this at
// glslang mapIO; the relaxed parse drops the qualifiers, so the location assigner
// enforces it - this is the experiment's synthetic divergence case.
TEST_F(ProgramTest, ExplicitUniformLocationOverlapAcrossStagesFailsLink) {
const char* vsSource = R"(#version 460 core
layout(location = 3) uniform vec4 uVec[4];
void main() { gl_Position = uVec[0] + uVec[3]; }
)";
const char* fsSource = R"(#version 460 core
layout(location = 5) uniform float uF;
out vec4 fragColor;
void main() { fragColor = vec4(uF); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_FALSE);
char infoLog[1024] = "";
GLsizei logLength = 0;
GetProgramInfoLog(program, sizeof(infoLog), &logLength, infoLog);
EXPECT_GT(logLength, 0);
}
// The same uniform declared with different explicit locations in two stages is a
// link error as well.
TEST_F(ProgramTest, ConflictingExplicitUniformLocationsOnSameUniformFailLink) {
const char* vsSource = R"(#version 460 core
layout(location = 2) uniform vec4 uShared;
void main() { gl_Position = uShared; }
)";
const char* fsSource = R"(#version 460 core
layout(location = 4) uniform vec4 uShared;
out vec4 fragColor;
void main() { fragColor = uShared; }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
(void)LinkVsFs(vs, fs, GL_FALSE);
}
// Same-location explicit declarations of the SAME uniform in both stages stay
// linkable, and both explicit locations (opaque and non-opaque) are honored.
TEST_F(ProgramTest, ExplicitUniformLocationsHonoredForPlainAndOpaqueUniforms) {
const char* vsSource = R"(#version 460 core
layout(location = 11) uniform mat4 uMvp;
void main() { gl_Position = uMvp * vec4(1.0); }
)";
const char* fsSource = R"(#version 460 core
layout(location = 7) uniform sampler2D uTex;
layout(location = 11) uniform mat4 uMvp;
out vec4 fragColor;
void main() { fragColor = texture(uTex, uMvp[0].xy); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
EXPECT_EQ(GetUniformLocation(program, "uMvp"), 11);
EXPECT_EQ(GetUniformLocation(program, "uTex"), 7);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// A glslang-auto-assigned opaque location may collide with a source-explicit plain
// uniform location under the relaxed parse (glslang no longer sees the plain
// uniform's qualifier). The assigner must relocate the auto one, not fail the link.
TEST_F(ProgramTest, AutoOpaqueLocationCollidingWithExplicitPlainLocationRelocates) {
const char* vsSource = R"(#version 460 core
layout(location = 0) uniform mat4 uM;
void main() { gl_Position = uM * vec4(1.0); }
)";
const char* fsSource = R"(#version 460 core
uniform sampler2D uTex;
out vec4 fragColor;
void main() { fragColor = texture(uTex, vec2(0.5)); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
const GLint mLoc = GetUniformLocation(program, "uM");
const GLint texLoc = GetUniformLocation(program, "uTex");
EXPECT_EQ(mLoc, 0);
ASSERT_NE(texLoc, -1);
EXPECT_NE(texLoc, mLoc);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Relinking a program and linking the same compiled shaders into a second program
// both re-consume the stored single parse (glslang mapIO mutates a linked TShader,
// so reuse goes through the consume-once re-parse path). Reflection must be intact
// every time, without any glCompileShader in between.
TEST_F(ProgramTest, RelinkAndSecondProgramReuseCompiledShaders) {
const char* vsSource = R"(#version 330 core
uniform mat4 uMvp;
in vec3 aPos;
void main() { gl_Position = uMvp * vec4(aPos, 1.0); }
)";
const char* fsSource = R"(#version 330 core
uniform sampler2D uTex;
uniform vec4 uTint;
out vec4 fragColor;
void main() { fragColor = texture(uTex, vec2(0.5)) * uTint; }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program1 = LinkVsFs(vs, fs, GL_TRUE);
GLint activeUniforms1 = 0;
GetProgramiv(program1, GL_ACTIVE_UNIFORMS, &activeUniforms1);
EXPECT_EQ(activeUniforms1, 3);
EXPECT_NE(GetUniformLocation(program1, "uMvp"), -1);
// Relink: consumes the re-parse path.
LinkProgram(program1);
GLint relinkStatus = GL_FALSE;
char infoLog[1024] = "";
GetProgramiv(program1, GL_LINK_STATUS, &relinkStatus);
GetProgramInfoLog(program1, sizeof(infoLog), nullptr, infoLog);
ASSERT_EQ(relinkStatus, GL_TRUE) << infoLog;
GLint activeUniformsRelink = 0;
GetProgramiv(program1, GL_ACTIVE_UNIFORMS, &activeUniformsRelink);
EXPECT_EQ(activeUniformsRelink, 3);
EXPECT_NE(GetUniformLocation(program1, "uTint"), -1);
// Same shaders into a fresh program.
GLuint program2 = LinkVsFs(vs, fs, GL_TRUE);
GLint activeUniforms2 = 0;
GetProgramiv(program2, GL_ACTIVE_UNIFORMS, &activeUniforms2);
EXPECT_EQ(activeUniforms2, 3);
EXPECT_NE(GetUniformLocation(program2, "uTex"), -1);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Programs and shaders share one GL name space (GL 3.3 core 2.11). A name must
// never be handed out as both, and a shader name passed where a program is
// expected is INVALID_OPERATION (KHR-GL30.get_uniform_tests.get_uniform relies
// on this; a name-collided linked program used to swallow the error).
TEST_F(ProgramTest, ProgramAndShaderNamesShareOneNameSpace) {
GLuint program = CreateProgram();
GLuint vs = CreateShader(GL_VERTEX_SHADER);
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
EXPECT_NE(program, vs);
EXPECT_NE(program, fs);
EXPECT_NE(vs, fs);
EXPECT_EQ(IsProgram(vs), GL_FALSE);
EXPECT_EQ(IsShader(program), GL_FALSE);
GLfloat floatValue = 0.0f;
GetUniformfv(vs, 0, &floatValue);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
GLint intValue = 0;
GetUniformiv(fs, 0, &intValue);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_INVALID_OPERATION));
// A never-allocated name is INVALID_VALUE, distinguishing the two cases.
GetUniformfv(program + vs + fs + 100, 0, &floatValue);
EXPECT_EQ(GetError(), static_cast<GLenum>(GL_INVALID_VALUE));
DeleteShader(vs);
DeleteShader(fs);
DeleteProgram(program);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---- builtin-shadowing OpName pass (P0c) ----
// Desktop GLSL lets a pack redefine builtins; ESSL 3.x forbids it, so the rename
// now happens as a SPIR-V OpName pass in SanitizeAndOptimizeBinary instead of the
// old whole-source string scan. These pin the pass end-to-end: real sources through
// glCompileShader/glLinkProgram, generated SPIR-V transpiled to the ESSL the Espryt
// driver would see.
namespace {
Vector<MobileGL::String> TranspileProgramSpirvToEssl(GLuint program) {
Vector<MobileGL::String> esslModules;
auto programObj = MG_State::pGLContext->GetProgramObject(program);
for (auto& spirvCode : programObj->GetGeneratedSpirv()) {
MG_Util::ShaderTranspiler::SpvcSession spvcSession(
spirvCode, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
spvc_compiler_options options;
spvcSession.CreateOptions(&options);
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
spvcSession.SetOptions(options);
const char* result = nullptr;
spvcSession.Compile(&result);
EXPECT_NE(result, nullptr) << spvcSession.GetLastErrorString();
esslModules.push_back(result ? result : "");
}
return esslModules;
}
} // namespace
// The two blind spots of the old string scan, eliminated by construction: a
// MULTILINE definition (bliss-shaped "float fma\n(...)"), and names outside the
// old 5-entry list: sinh, as a NEW overload no builtin signature matches, so it
// parses fine and the SPIR-V OpName backstop does the rename. (An EXACT-signature
// sinh redefinition is parse-rejected by glslang - on HEAD too - and is therefore
// deliberately NOT lexically rescued; see kLexicalPreemptRenameNames.)
// min3/max3 keep their historical coverage.
TEST_F(ProgramTest, BuiltinShadowingFunctionsRenamedInEsslOutput) {
const char* vsSource = R"(#version 330 core
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
)";
const char* fsSource = R"(#version 330 core
out vec4 fragColor;
float fma
(float a, float b, float c) { return a * b + c; }
float sinh(float x, float y) { return x * y; }
float length_squared(vec3 value) { return dot(value, value); }
float round(float x) { return floor(x + 0.5); }
float min3(float a, float b, float c) { return min(min(a, b), c); }
void main() {
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25),
min3(0.1, 0.2, 0.3) + length_squared(vec3(0.1, 0.2, 0.3)));
}
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
if (essl.find("fragColor") == String::npos) continue; // fragment module only
EXPECT_NE(essl.find("mg_fma("), String::npos) << essl;
EXPECT_NE(essl.find("mg_sinh("), String::npos) << essl;
EXPECT_NE(essl.find("mg_length_squared("), String::npos) << essl;
EXPECT_NE(essl.find("mg_round("), String::npos) << essl;
EXPECT_NE(essl.find("mg_min3("), String::npos) << essl;
EXPECT_EQ(essl.find("float fma("), String::npos) << essl;
EXPECT_EQ(essl.find("float sinh("), String::npos) << essl;
EXPECT_EQ(essl.find("float round("), String::npos) << essl;
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Pure builtin USAGE (plus a commented-out definition) must stay untouched: builtin
// calls never resolve to a user function id in SPIR-V, so no mg_ name may appear.
TEST_F(ProgramTest, BuiltinUsageWithoutShadowingDefinitionKeepsBuiltinCalls) {
const char* vsSource = R"(#version 330 core
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
)";
// 400, not 330: the builtin fma() really is called here, and it is only core from GLSL 4.00
// (at 330 it needs GL_ARB_gpu_shader5). The shadowing case above can stay at 330 precisely
// because the rename means no call to the builtin survives.
const char* fsSource = R"(#version 400 core
// float round(float x) { return floor(x + 0.5); }
out vec4 fragColor;
void main() {
fragColor = vec4(round(1.25), fma(0.1, 0.2, 0.3), tanh(0.5), 1.0);
}
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
EXPECT_EQ(essl.find("mg_"), String::npos) << essl;
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---- the three shapes the lexical pre-empt pass must NOT touch (P0c) ----
// The source-level rename runs only for the handful of names glslang's relaxed
// parse rejects outright; everything else waits for the OpName pass, which cannot
// over-fire. These pin the three ways a lexical scan gets it wrong. All of them
// would fail as "no matching overloaded function found" - an over-detection is
// unrecoverable because the source never reaches SPIR-V.
namespace {
// "pow(" as a real builtin call, i.e. not the tail of "mg_pow(".
bool ContainsUnprefixedCall(const MobileGL::String& essl, const MobileGL::String& name) {
const MobileGL::String needle = name + "(";
for (SizeT pos = essl.find(needle); pos != String::npos; pos = essl.find(needle, pos + 1)) {
const char before = pos == 0 ? ' ' : essl[pos - 1];
const bool isIdentifierChar =
std::isalnum(static_cast<unsigned char>(before)) != 0 || before == '_';
if (!isIdentifierChar) return true;
}
return false;
}
} // namespace
// B1: preprocessor-asymmetric braces desync a raw brace-depth counter (each arm of
// the #ifdef closes the function), and "return" is lexically an identifier - so
// "return clamp(...)" reads as a top-level definition "<type> <builtin> (". A
// shader that shadows nothing must survive intact.
TEST_F(ProgramTest, StatementKeywordCallInPreprocessorAsymmetricBracesIsNotAShadowingDefinition) {
const char* vsSource = R"(#version 330 core
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
)";
const char* fsSource = R"(#version 330 core
uniform vec3 uP;
out vec4 fragColor;
float getShadow(vec3 v) {
#ifdef SHADOW_OFF
return 1.0;
}
#else
return round(dot(v, v));
}
#endif
void main() { fragColor = vec4(getShadow(uP) * clamp(uP.x, 0.0, 1.0)); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
if (essl.find("fragColor") == String::npos) continue; // fragment module only
EXPECT_EQ(essl.find("mg_"), String::npos) << essl;
// SPIRV-Cross lowers GLSL.std.450 FClamp to its NaN-correct min/max/isnan form, so the
// surviving evidence of the builtin call is that pair, not the spelling "clamp(". The
// stronger guard is above it: a renamed mg_clamp would not have compiled at all.
EXPECT_TRUE(ContainsUnprefixedCall(essl, "min")) << essl;
EXPECT_TRUE(ContainsUnprefixedCall(essl, "max")) << essl;
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// B2: the scan is preprocessor-blind, so a definition in a DEAD #if branch would
// poison every live call to the real builtin. #version 120 normalizes to 330, so
// __VERSION__ is 330 and the compat shim is dropped by glslang - the definition
// never exists, and nothing may be renamed.
TEST_F(ProgramTest, ShadowingDefinitionInDeadPreprocessorBranchLeavesLiveBuiltinCalls) {
const char* vsSource = R"(#version 120
#if __VERSION__ < 140
mat4 inverse(mat4 m) { return m; }
#endif
uniform mat4 uM;
uniform vec4 uV;
void main() { gl_Position = inverse(uM) * uV; }
)";
const char* fsSource = R"(#version 330 core
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
if (essl.find("gl_Position") == String::npos) continue; // vertex module only
EXPECT_EQ(essl.find("mg_"), String::npos) << essl;
EXPECT_TRUE(ContainsUnprefixedCall(essl, "inverse")) << essl;
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// B3: the idiomatic reason to shadow a builtin is to ADD an overload and delegate
// to the real one. A blanket call-site rewrite would turn the body's builtin call
// into mg_pow(vec3, vec3), which has no overload. The OpName backstop renames the
// user function id only, so the delegation still resolves to GLSL.std.450 Pow.
TEST_F(ProgramTest, OverloadDelegatingToShadowedBuiltinKeepsItsBuiltinCall) {
const char* vsSource = R"(#version 330 core
void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); }
)";
const char* fsSource = R"(#version 330 core
uniform vec3 uBase;
out vec4 fragColor;
vec3 pow(vec3 v, float e) { return pow(v, vec3(e)); }
void main() { fragColor = vec4(pow(uBase, 2.2), 1.0); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
for (const auto& essl : TranspileProgramSpirvToEssl(program)) {
if (essl.find("fragColor") == String::npos) continue; // fragment module only
EXPECT_NE(essl.find("mg_pow("), String::npos) << essl;
EXPECT_TRUE(ContainsUnprefixedCall(essl, "pow")) << essl;
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------
// P0b: source-hash dedupe for shader recompiles.
// Layer 1 - the same shader object re-sourced with byte-identical text keeps its
// compiled state, and glCompileShader on it is a no-op.
// Layer 2 - two DIFFERENT shader objects holding byte-identical text share the
// source-only half of the pipeline (preprocess + lexical checks +
// side-channel extraction) through the context's ShaderPreprocessCache,
// while each still gets its own glslang parse.
// ---------------------------------------------------------------------------
namespace {
const char* kP0bVs = R"(#version 330 core
uniform mat4 uModel;
uniform vec4 uTint;
void main() { gl_Position = uModel * uTint; }
)";
const char* kP0bFs = R"(#version 330 core
uniform vec4 uColor;
out vec4 fragColor;
void main() { fragColor = uColor; }
)";
// Same stage, different declared uniform: makes "did it actually recompile?"
// observable through reflection rather than through internal state.
const char* kP0bAltFs = R"(#version 330 core
uniform vec4 uOtherColor;
out vec4 fragColor;
void main() { fragColor = uOtherColor; }
)";
const char* kP0bBrokenFs = R"(#version 330 core
out vec4 fragColor;
void main() { fragColor = notADeclaredThing; }
)";
GLuint MakeShaderWithSource(GLenum type, const char* source) {
GLuint shader = CreateShader(type);
ShaderSource(shader, 1, &source, nullptr);
return shader;
}
GLint QueryCompileStatus(GLuint shader) {
GLint status = GL_FALSE;
GetShaderiv(shader, GL_COMPILE_STATUS, &status);
return status;
}
String QueryShaderInfoLog(GLuint shader) {
GLint length = 0;
GetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
if (length <= 0) return String();
std::vector<GLchar> buffer(static_cast<size_t>(length));
GLsizei written = 0;
GetShaderInfoLog(shader, length, &written, buffer.data());
return String(buffer.data(), static_cast<size_t>(written));
}
Bool ShaderHasMemoizedCompile(GLuint shader) {
const auto& shaderObject = MG_State::pGLContext->GetShaderObject(shader);
EXPECT_NE(shaderObject, nullptr);
return shaderObject != nullptr && shaderObject->HasMemoizedCompile();
}
} // namespace
// Layer 1, success path: re-sourcing with identical text and recompiling must leave
// COMPILE_STATUS, the info log and every downstream consumer exactly as they were -
// including a program that links the shader AFTER the redundant recompile.
TEST_F(ProgramTest, RecompileWithIdenticalSourceKeepsCompiledStateAndStillLinks) {
GLuint vs = MakeShaderWithSource(GL_VERTEX_SHADER, kP0bVs);
GLuint fs = MakeShaderWithSource(GL_FRAGMENT_SHADER, kP0bFs);
CompileShader(vs);
CompileShader(fs);
ASSERT_EQ(QueryCompileStatus(vs), GL_TRUE) << QueryShaderInfoLog(vs);
ASSERT_EQ(QueryCompileStatus(fs), GL_TRUE) << QueryShaderInfoLog(fs);
const String vsLogBefore = QueryShaderInfoLog(vs);
EXPECT_TRUE(ShaderHasMemoizedCompile(vs));
// A first link consumes the stored TShader; the redundant recompile below must not
// disturb the preprocessed source that ClaimParsedShader re-parses from.
GLuint firstProgram = LinkVsFs(vs, fs, GL_TRUE);
EXPECT_GE(GetUniformLocation(firstProgram, "uColor"), 0);
// glShaderSource with byte-identical text, then glCompileShader: both no-ops.
ShaderSource(vs, 1, &kP0bVs, nullptr);
EXPECT_TRUE(ShaderHasMemoizedCompile(vs)) << "identical re-source must not invalidate the compiled state";
CompileShader(vs);
ShaderSource(fs, 1, &kP0bFs, nullptr);
CompileShader(fs);
EXPECT_EQ(QueryCompileStatus(vs), GL_TRUE);
EXPECT_EQ(QueryCompileStatus(fs), GL_TRUE);
EXPECT_EQ(QueryShaderInfoLog(vs), vsLogBefore);
// The original source text is still what glGetShaderSource reports.
GLint sourceLength = 0;
GetShaderiv(vs, GL_SHADER_SOURCE_LENGTH, &sourceLength);
ASSERT_GT(sourceLength, 1);
std::vector<GLchar> sourceBuffer(static_cast<size_t>(sourceLength));
GLsizei written = 0;
GetShaderSource(vs, sourceLength, &written, sourceBuffer.data());
EXPECT_EQ(String(sourceBuffer.data(), static_cast<size_t>(written)), String(kP0bVs));
// A second program built from the same, redundantly recompiled shaders links and
// reflects - i.e. ClaimParsedShader's re-parse path survived the no-op.
GLuint secondProgram = LinkVsFs(vs, fs, GL_TRUE);
EXPECT_GE(GetUniformLocation(secondProgram, "uColor"), 0);
EXPECT_GE(GetUniformLocation(secondProgram, "uModel"), 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Layer 1 must not swallow a REAL source change: different text invalidates, and the
// change is visible in what the next link reflects.
TEST_F(ProgramTest, DifferentSourceAfterCompileInvalidatesCompiledState) {
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, kP0bVs);
GLuint fs = MakeShaderWithSource(GL_FRAGMENT_SHADER, kP0bFs);
CompileShader(fs);
ASSERT_EQ(QueryCompileStatus(fs), GL_TRUE) << QueryShaderInfoLog(fs);
GLuint firstProgram = LinkVsFs(vs, fs, GL_TRUE);
EXPECT_GE(GetUniformLocation(firstProgram, "uColor"), 0);
EXPECT_EQ(GetUniformLocation(firstProgram, "uOtherColor"), -1);
// New text -> compiled state gone, and glCompileShader is mandatory again.
ShaderSource(fs, 1, &kP0bAltFs, nullptr);
EXPECT_FALSE(ShaderHasMemoizedCompile(fs));
EXPECT_EQ(QueryCompileStatus(fs), GL_FALSE);
CompileShader(fs);
ASSERT_EQ(QueryCompileStatus(fs), GL_TRUE) << QueryShaderInfoLog(fs);
GLuint secondProgram = LinkVsFs(vs, fs, GL_TRUE);
EXPECT_GE(GetUniformLocation(secondProgram, "uOtherColor"), 0);
EXPECT_EQ(GetUniformLocation(secondProgram, "uColor"), -1);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Layer 2: byte-identical source in two distinct shader objects. Both must compile,
// and each must own an independent TShader - if the parse were shared, the second
// link would be handed an intermediate that the first link's mapIO already mutated.
TEST_F(ProgramTest, TwoShaderObjectsWithIdenticalSourceLinkIndependently) {
GLuint vsA = CompileShaderChecked(GL_VERTEX_SHADER, kP0bVs);
GLuint fsA = CompileShaderChecked(GL_FRAGMENT_SHADER, kP0bFs);
GLuint vsB = CompileShaderChecked(GL_VERTEX_SHADER, kP0bVs);
GLuint fsB = CompileShaderChecked(GL_FRAGMENT_SHADER, kP0bFs);
ASSERT_NE(vsA, vsB);
ASSERT_NE(fsA, fsB);
const auto& objectA = MG_State::pGLContext->GetShaderObject(vsA);
const auto& objectB = MG_State::pGLContext->GetShaderObject(vsB);
ASSERT_NE(objectA, nullptr);
ASSERT_NE(objectB, nullptr);
EXPECT_EQ(objectA->GetShaderSource(), objectB->GetShaderSource());
// P0b's layer 2 shares the PREPROCESS and never the parse: glslang's TShader is
// consume-once, so a memo hit still has to parse for itself.
//
// P1 stage 6 shares something stronger when it is active - the whole compile JOB, and
// therefore the single parse that job produced - and that sharing is made safe by
// ShaderCompileTask::ClaimParsedShader's CAS instead, exactly as it already was for one
// shader object attached to two programs. ShaderCompileAdoptionTest is where that is
// pinned down (it links both objects and compares the generated SPIR-V). So the
// one-parse-per-object assertion belongs to the non-adopting path; the two independent
// LINKS below are what both modes have to agree on, and they are the point of this case.
if (!MG_Util::Async::AsyncShaderCompileActive()) {
EXPECT_NE(objectA->GetCompiledShader(), objectB->GetCompiledShader());
}
EXPECT_NE(objectA->GetCompiledShader(), nullptr);
EXPECT_NE(objectB->GetCompiledShader(), nullptr);
GLuint programA = LinkVsFs(vsA, fsA, GL_TRUE);
GLuint programB = LinkVsFs(vsB, fsB, GL_TRUE);
for (GLuint program : {programA, programB}) {
EXPECT_GE(GetUniformLocation(program, "uColor"), 0);
EXPECT_GE(GetUniformLocation(program, "uModel"), 0);
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// Failure memoization: a compile that failed stays failed, with the SAME log, when
// recompiled against the same source; a real fix to the source still takes effect.
// The second object pins the cached-ParseFailed path (layer 2), which skips the parse
// entirely and must reproduce the identical verdict.
TEST_F(ProgramTest, FailedCompileIsMemoizedAndStillRecoversOnGoodSource) {
GLuint fs = MakeShaderWithSource(GL_FRAGMENT_SHADER, kP0bBrokenFs);
CompileShader(fs);
ASSERT_EQ(QueryCompileStatus(fs), GL_FALSE);
const String failureLog = QueryShaderInfoLog(fs);
EXPECT_FALSE(failureLog.empty());
// Layer 1: identical re-source + recompile keeps the failure AND the log queryable.
ShaderSource(fs, 1, &kP0bBrokenFs, nullptr);
CompileShader(fs);
EXPECT_EQ(QueryCompileStatus(fs), GL_FALSE);
EXPECT_EQ(QueryShaderInfoLog(fs), failureLog);
// Layer 2: a second object with the same broken source reports the same failure.
GLuint otherFs = MakeShaderWithSource(GL_FRAGMENT_SHADER, kP0bBrokenFs);
CompileShader(otherFs);
EXPECT_EQ(QueryCompileStatus(otherFs), GL_FALSE);
EXPECT_EQ(QueryShaderInfoLog(otherFs), failureLog);
// A genuine fix still compiles and links.
ShaderSource(fs, 1, &kP0bFs, nullptr);
CompileShader(fs);
ASSERT_EQ(QueryCompileStatus(fs), GL_TRUE) << QueryShaderInfoLog(fs);
EXPECT_TRUE(QueryShaderInfoLog(fs).empty());
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, kP0bVs);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
EXPECT_GE(GetUniformLocation(program, "uColor"), 0);
}
// Layer 2 under eviction: push more distinct sources through the context than the
// cache can hold, then confirm nothing broke and a fresh duplicate pair still works.
TEST_F(ProgramTest, PreprocessCacheOverflowKeepsCompilingCorrectly) {
const SizeT overflow = MG_State::GLState::ShaderPreprocessCache::kMaxEntries + 8;
for (SizeT i = 0; i < overflow; ++i) {
const String source = "#version 330 core\nuniform vec4 uColor" + ToString(i) +
";\nout vec4 fragColor;\nvoid main() { fragColor = uColor" + ToString(i) + "; }\n";
const char* sourcePtr = source.c_str();
GLuint shader = MakeShaderWithSource(GL_FRAGMENT_SHADER, sourcePtr);
CompileShader(shader);
ASSERT_EQ(QueryCompileStatus(shader), GL_TRUE) << QueryShaderInfoLog(shader) << "\n" << source;
DeleteShader(shader);
}
// Everything inserted above has long since been evicted; a brand-new duplicate
// pair must still take the layer-2 path and produce two working programs.
GLuint vsA = CompileShaderChecked(GL_VERTEX_SHADER, kP0bVs);
GLuint fsA = CompileShaderChecked(GL_FRAGMENT_SHADER, kP0bFs);
GLuint vsB = CompileShaderChecked(GL_VERTEX_SHADER, kP0bVs);
GLuint fsB = CompileShaderChecked(GL_FRAGMENT_SHADER, kP0bFs);
GLuint programA = LinkVsFs(vsA, fsA, GL_TRUE);
GLuint programB = LinkVsFs(vsB, fsB, GL_TRUE);
EXPECT_GE(GetUniformLocation(programA, "uColor"), 0);
EXPECT_GE(GetUniformLocation(programB, "uColor"), 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// glUniformMatrix{2x3,2x4,3x2,3x4,4x2,4x3}fv and their twelve glProgramUniformMatrix* twins were
// validate-only no-ops: they never took the value pointer at all. They upload column-at-a-time at
// the std140 16-byte column stride, honouring `transpose`, and glGetUniformfv undoes that padding.
TEST_F(ProgramTest, NonSquareMatrixUniformsRoundTripThroughTheGlobalUbo) {
const char* vsSource = R"(#version 430 core
uniform mat2x3 uM2x3;
uniform mat3x2 uM3x2;
uniform mat4x3 uM4x3;
uniform mat2 uM2;
void main() {
vec3 a = uM2x3 * vec2(1.0);
vec2 b = uM3x2 * vec3(1.0);
vec3 c = uM4x3 * vec4(1.0);
vec2 d = uM2 * vec2(1.0);
gl_Position = vec4(a.xy + b + c.xy + d, 0.0, 1.0);
}
)";
const char* fsSource = R"(#version 430 core
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
UseProgram(program);
ASSERT_EQ(GetError(), GL_NO_ERROR);
// matCxR is C columns of R rows, column-major: value[c * R + r].
const GLfloat m2x3[6] = {1, 2, 3, 4, 5, 6};
const GLfloat m3x2[6] = {1, 2, 3, 4, 5, 6};
const GLfloat m4x3[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
const GLint loc2x3 = GetUniformLocation(program, "uM2x3");
const GLint loc3x2 = GetUniformLocation(program, "uM3x2");
const GLint loc4x3 = GetUniformLocation(program, "uM4x3");
ASSERT_GE(loc2x3, 0);
ASSERT_GE(loc3x2, 0);
ASSERT_GE(loc4x3, 0);
UniformMatrix2x3fv(loc2x3, 1, GL_FALSE, m2x3);
UniformMatrix3x2fv(loc3x2, 1, GL_FALSE, m3x2);
UniformMatrix4x3fv(loc4x3, 1, GL_FALSE, m4x3);
ASSERT_EQ(GetError(), GL_NO_ERROR);
GLfloat readBack[12] = {};
GetUniformfv(program, loc2x3, readBack);
EXPECT_EQ(std::memcmp(readBack, m2x3, sizeof(m2x3)), 0);
std::memset(readBack, 0, sizeof(readBack));
GetUniformfv(program, loc3x2, readBack);
EXPECT_EQ(std::memcmp(readBack, m3x2, sizeof(m3x2)), 0);
std::memset(readBack, 0, sizeof(readBack));
GetUniformfv(program, loc4x3, readBack);
EXPECT_EQ(std::memcmp(readBack, m4x3, sizeof(m4x3)), 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
// transpose = GL_TRUE means the source is row-major: a mat3x2 (3 columns, 2 rows) is then
// given as 2 rows of 3, so {1,2,3, 4,5,6} is the column-major {1,4, 2,5, 3,6}.
UniformMatrix3x2fv(loc3x2, 1, GL_TRUE, m3x2);
const GLfloat expectedTransposed3x2[6] = {1, 4, 2, 5, 3, 6};
std::memset(readBack, 0, sizeof(readBack));
GetUniformfv(program, loc3x2, readBack);
EXPECT_EQ(std::memcmp(readBack, expectedTransposed3x2, sizeof(expectedTransposed3x2)), 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
// The glProgramUniform* twin writes the same bytes without the program being current.
UseProgram(0);
const GLfloat other2x3[6] = {9, 8, 7, 6, 5, 4};
ProgramUniformMatrix2x3fv(program, loc2x3, 1, GL_FALSE, other2x3);
std::memset(readBack, 0, sizeof(readBack));
GetUniformfv(program, loc2x3, readBack);
EXPECT_EQ(std::memcmp(readBack, other2x3, sizeof(other2x3)), 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// A mat2 is not four contiguous floats in the global UBO: std140 pads each column vector out to
// 16 bytes, so column 1 starts at byte 16. Writing it packed put column 1 on top of column 0's
// padding, where the shader never reads it.
TEST_F(ProgramTest, Mat2UniformUsesTheStd140ColumnStride) {
const char* vsSource = R"(#version 430 core
uniform mat2 uM2;
void main() { gl_Position = vec4(uM2 * vec2(1.0), 0.0, 1.0); }
)";
const char* fsSource = R"(#version 430 core
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
)";
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
GLuint fs = CompileShaderChecked(GL_FRAGMENT_SHADER, fsSource);
GLuint program = LinkVsFs(vs, fs, GL_TRUE);
UseProgram(program);
const GLint loc = GetUniformLocation(program, "uM2");
ASSERT_GE(loc, 0);
const GLfloat m2[4] = {1, 2, 3, 4};
UniformMatrix2fv(loc, 1, GL_FALSE, m2);
ASSERT_EQ(GetError(), GL_NO_ERROR);
// The GL-visible value is tightly packed...
GLfloat readBack[4] = {};
GetUniformfv(program, loc, readBack);
EXPECT_EQ(std::memcmp(readBack, m2, sizeof(m2)), 0);
// ...while the bytes in the UBO put column 1 at offset 16, not 8.
const auto& programObject = MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(programObject, nullptr);
const auto* ubo = static_cast<const char*>(programObject->MapUBO());
ASSERT_NE(ubo, nullptr);
const Uint offset = programObject->GetUniformOffset(static_cast<Uint>(loc));
ASSERT_NE(offset, MG_State::GLState::ProgramObject::kInvalidUniformOffset);
GLfloat column0[2] = {};
GLfloat column1[2] = {};
std::memcpy(column0, ubo + offset, sizeof(column0));
std::memcpy(column1, ubo + offset + 16, sizeof(column1));
EXPECT_FLOAT_EQ(column0[0], 1.0f);
EXPECT_FLOAT_EQ(column0[1], 2.0f);
EXPECT_FLOAT_EQ(column1[0], 3.0f);
EXPECT_FLOAT_EQ(column1[1], 4.0f);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// GL 4.6 core 7.1: shaderType is an enum, so an unrecognised one is INVALID_ENUM - it used to be
// reported as INVALID_VALUE. glCreateShaderProgramv adds a count < 0 gate ahead of everything.
TEST_F(ProgramTest, CreateShaderAndCreateShaderProgramvReportTheRightErrorClasses) {
while (GetError() != GL_NO_ERROR) {
}
EXPECT_EQ(CreateShader(GL_FLOAT), 0u);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error";
const char* source = "#version 330 core\nvoid main() { gl_Position = vec4(1.0); }\n";
EXPECT_EQ(CreateShaderProgramv(GL_FLOAT, 1, &source), 0u);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error";
EXPECT_EQ(CreateShaderProgramv(GL_VERTEX_SHADER, -1, &source), 0u);
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error";
// A well-formed call still works.
const GLuint program = CreateShaderProgramv(GL_VERTEX_SHADER, 1, &source);
EXPECT_NE(program, 0u);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}