[Test] (GlSpirv): unit and headless-GPU coverage for glShaderBinary, glSpecializeShader and the SPIR_V_BINARY state

This commit is contained in:
2026-08-27 05:37:18 -04:00
parent 6f299372c6
commit e315d9e798
3 changed files with 552 additions and 0 deletions
@@ -108,6 +108,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/AtomicCounterScenario.cpp
Scenarios/SsboArrayDynamicIndexScenario.cpp
Scenarios/StorageBufferRegrowScenario.cpp
Scenarios/SpirvShaderBinaryScenario.cpp
Scenarios/RelinkStageSetScenario.cpp
Scenarios/GuiBatchScenario.cpp
Scenarios/UnboundImageDescriptorScenario.cpp
@@ -0,0 +1,324 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - AN APPLICATION-SUPPLIED SPIR-V MODULE RENDERS, END TO END.
//
// GL_ARB_gl_spirv is core in 4.6 and MobileGL advertises a 4.6 context, but glShaderBinary and
// glSpecializeShader were DECLARE_GL_FUNCTION_STUB entry points: they took their arguments,
// recorded no error and did nothing, and glGetShaderiv(GL_SPIR_V_BINARY) raised GL_INVALID_ENUM.
// Every gl_spirv conformance body died on the first of those two calls.
//
// This scenario is the end-to-end proof that the path now WORKS rather than merely answers: two
// modules that glslang compiled ahead of time (embedded below as words, so the test depends on
// no toolchain at run time), handed to glShaderBinary, specialized with a scale and a channel
// index, linked, drawn, and read back. It runs on both backends and, in CI, on llvmpipe/lavapipe.
//
// The two specialization constants are the load-bearing part. The vertex module scales its
// position by constant id 3 and the fragment module writes 1.0 into the channel named by constant
// id 7 - so a specialization that silently did nothing would leave the default scale of 1.0 (a
// full-viewport quad instead of a quarter-sized one) and the default channel 0 (red instead of
// green), and BOTH would show up in the readback. A "specialization" that merely stored the
// values without folding them in is exactly the failure mode this shape is built to catch.
//
// The GLSL the modules came from:
// vertex: layout(location = 0) in vec2 aPos;
// layout(constant_id = 3) const float uScale = 1.0;
// void main() { gl_Position = vec4(aPos * uScale, 0.0, 1.0); }
// fragment: layout(location = 0) out vec4 oColor;
// layout(constant_id = 7) const int uChannel = 0;
// void main() { vec4 c = vec4(0,0,0,1); c[uChannel] = 1.0; oColor = c; }
// compiled with `glslangValidator -G --target-env opengl`.
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
#ifndef GL_SHADER_BINARY_FORMAT_SPIR_V
#define GL_SHADER_BINARY_FORMAT_SPIR_V 0x9551
#endif
#ifndef GL_SPIR_V_BINARY
#define GL_SPIR_V_BINARY 0x9552
#endif
namespace MGITest {
namespace {
class SpirvShaderBinaryScenario : public ScenarioTest {};
// 255 words
const unsigned int kVertexModule[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000020u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
0x0009000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000012u, 0x0000001eu,
0x0000001fu, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu,
0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u,
0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u,
0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, 0x00040005u, 0x00000012u, 0x736f5061u, 0x00000000u,
0x00040005u, 0x00000014u, 0x61635375u, 0x0000656cu, 0x00050005u, 0x0000001eu, 0x565f6c67u, 0x65747265u,
0x00444978u, 0x00060005u, 0x0000001fu, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x00000044u, 0x00030047u,
0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u,
0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu,
0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000012u,
0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x00000001u, 0x00000003u, 0x00040047u, 0x0000001eu,
0x0000000bu, 0x00000005u, 0x00040047u, 0x0000001fu, 0x0000000bu, 0x00000006u, 0x00020013u, 0x00000002u,
0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u,
0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u,
0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu, 0x0000000bu,
0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu,
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u,
0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u, 0x00000006u, 0x00000002u,
0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000001u,
0x00040032u, 0x00000006u, 0x00000014u, 0x3f800000u, 0x0004002bu, 0x00000006u, 0x00000016u, 0x00000000u,
0x0004002bu, 0x00000006u, 0x00000017u, 0x3f800000u, 0x00040020u, 0x0000001bu, 0x00000003u, 0x00000007u,
0x00040020u, 0x0000001du, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x0000001du, 0x0000001eu, 0x00000001u,
0x0004003bu, 0x0000001du, 0x0000001fu, 0x00000001u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000010u, 0x00000013u, 0x00000012u, 0x0005008eu,
0x00000010u, 0x00000015u, 0x00000013u, 0x00000014u, 0x00050051u, 0x00000006u, 0x00000018u, 0x00000015u,
0x00000000u, 0x00050051u, 0x00000006u, 0x00000019u, 0x00000015u, 0x00000001u, 0x00070050u, 0x00000007u,
0x0000001au, 0x00000018u, 0x00000019u, 0x00000016u, 0x00000017u, 0x00050041u, 0x0000001bu, 0x0000001cu,
0x0000000du, 0x0000000fu, 0x0003003eu, 0x0000001cu, 0x0000001au, 0x000100fdu, 0x00010038u,
};
// 134 words
const unsigned int kFragmentModule[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000014u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
0x0006000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000012u, 0x00030010u, 0x00000004u,
0x00000008u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00030005u, 0x00000009u, 0x00000063u, 0x00050005u, 0x0000000eu, 0x61684375u, 0x6c656e6eu, 0x00000000u,
0x00040005u, 0x00000012u, 0x6c6f436fu, 0x0000726fu, 0x00040047u, 0x0000000eu, 0x00000001u, 0x00000007u,
0x00040047u, 0x00000012u, 0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u,
0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u,
0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x0004002bu, 0x00000006u, 0x0000000au, 0x00000000u,
0x0004002bu, 0x00000006u, 0x0000000bu, 0x3f800000u, 0x0007002cu, 0x00000007u, 0x0000000cu, 0x0000000au,
0x0000000au, 0x0000000au, 0x0000000bu, 0x00040015u, 0x0000000du, 0x00000020u, 0x00000001u, 0x00040032u,
0x0000000du, 0x0000000eu, 0x00000000u, 0x00040020u, 0x0000000fu, 0x00000007u, 0x00000006u, 0x00040020u,
0x00000011u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000003u, 0x00050036u,
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000008u,
0x00000009u, 0x00000007u, 0x0003003eu, 0x00000009u, 0x0000000cu, 0x00050041u, 0x0000000fu, 0x00000010u,
0x00000009u, 0x0000000eu, 0x0003003eu, 0x00000010u, 0x0000000bu, 0x0004003du, 0x00000007u, 0x00000013u,
0x00000009u, 0x0003003eu, 0x00000012u, 0x00000013u, 0x000100fdu, 0x00010038u,
};
// The quad the vertex module transforms. Full-viewport before the scale, so a scale of
// 0.5 covers exactly the middle half of each axis and the corners stay background.
const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
// The specialization constant ids the two modules declare.
constexpr unsigned int kScaleConstantId = 3;
constexpr unsigned int kChannelConstantId = 7;
unsigned int MakeSpirvShader(GLenum type, const unsigned int* words, size_t wordCount,
unsigned int constantId, unsigned int constantValue, std::string* outLog) {
const GLuint shader = glCreateShader(type);
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, words,
static_cast<GLsizei>(wordCount * sizeof(unsigned int)));
if (glGetError() != GL_NO_ERROR) {
if (outLog) *outLog = "glShaderBinary rejected the module";
glDeleteShader(shader);
return 0;
}
GLint isSpirv = GL_FALSE;
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
if (glGetError() != GL_NO_ERROR || isSpirv != GL_TRUE) {
if (outLog) *outLog = "GL_SPIR_V_BINARY did not read TRUE after glShaderBinary";
glDeleteShader(shader);
return 0;
}
glSpecializeShader(shader, "main", 1, &constantId, &constantValue);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled != GL_TRUE) {
if (outLog) {
GLint length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
std::vector<char> log(static_cast<size_t>(length > 0 ? length : 1), '\0');
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), nullptr, log.data());
*outLog = std::string(log.data());
}
glDeleteShader(shader);
return 0;
}
return shader;
}
} // namespace
TEST_F(SpirvShaderBinaryScenario, ShaderBinaryFormatIsAdvertisedExactlyOnce) {
if (!Ready()) return;
GLint formatCount = -1;
glGetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &formatCount);
EXPECT_EQ(FirstGLError(), 0u);
ASSERT_EQ(formatCount, 1) << "a 4.6 context supports exactly the SPIR-V shader binary format";
std::vector<GLint> formats(static_cast<size_t>(formatCount), 0);
glGetIntegerv(GL_SHADER_BINARY_FORMATS, formats.data());
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(formats[0], static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V))
<< "the count and the list have to describe the same thing";
}
TEST_F(SpirvShaderBinaryScenario, AnUnsupportedBinaryFormatIsRejectedInsteadOfSilentlyAccepted) {
if (!Ready()) return;
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
// 0x8DF9 is GL_SHADER_BINARY_FORMATS' neighbour, not a format: any value but
// GL_SHADER_BINARY_FORMAT_SPIR_V is GL_INVALID_ENUM. The stub used to return silently.
glShaderBinary(1, &shader, 0x8DF9, kVertexModule, sizeof(kVertexModule));
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_ENUM));
GLint isSpirv = GL_TRUE;
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(isSpirv, GL_FALSE) << "a rejected glShaderBinary must not have attached anything";
glDeleteShader(shader);
}
TEST_F(SpirvShaderBinaryScenario, CompileShaderOnASpirvShaderIsInvalidOperationAndShaderSourceTakesItBack) {
if (!Ready()) return;
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
ASSERT_EQ(FirstGLError(), 0u);
glCompileShader(shader);
EXPECT_EQ(FirstGLError(), static_cast<unsigned int>(GL_INVALID_OPERATION))
<< "glSpecializeShader, not glCompileShader, is what compiles a SPIR-V shader";
// glShaderSource takes the object back to being a GLSL shader, and GL_SPIR_V_BINARY with
// it - the transition the conformance suite checks explicitly.
const char* source = "#version 450\nvoid main() { gl_Position = vec4(0.0); }\n";
glShaderSource(shader, 1, &source, nullptr);
ASSERT_EQ(FirstGLError(), 0u);
GLint isSpirv = GL_TRUE;
glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(isSpirv, GL_FALSE);
glCompileShader(shader);
EXPECT_EQ(FirstGLError(), 0u) << "the object is an ordinary GLSL shader again";
glDeleteShader(shader);
}
TEST_F(SpirvShaderBinaryScenario, SpecializeShaderRejectsAConstantTheModuleDoesNotDeclare) {
if (!Ready()) return;
const GLuint shader = glCreateShader(GL_VERTEX_SHADER);
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
ASSERT_EQ(FirstGLError(), 0u);
// 4242 is not one of the module's constant ids. A specialization failure is a COMPILE
// failure, not a GL error - the extension routes it through COMPILE_STATUS.
const unsigned int badId = 4242;
const unsigned int value = 0;
glSpecializeShader(shader, "main", 1, &badId, &value);
GLint compiled = GL_TRUE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_FALSE);
GLint logLength = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
EXPECT_GT(logLength, 0) << "a failed specialization has to say why";
glDeleteShader(shader);
}
TEST_F(SpirvShaderBinaryScenario, SpecializedModulesLinkAndRenderWithTheirConstantsApplied) {
if (!Ready()) return;
HeadlessGL& gl = Gl();
const int width = gl.Width();
const int height = gl.Height();
ASSERT_GE(width, 16);
ASSERT_GE(height, 16);
std::string log;
// Scale 0.5 as a float, handed over as the GLuint bit pattern the extension specifies.
unsigned int halfBits = 0;
const float half = 0.5f;
std::memcpy(&halfBits, &half, sizeof(halfBits));
const unsigned int vs = MakeSpirvShader(GL_VERTEX_SHADER, kVertexModule,
sizeof(kVertexModule) / sizeof(kVertexModule[0]),
kScaleConstantId, halfBits, &log);
ASSERT_NE(vs, 0u) << "vertex: " << log;
// Channel 1 is green; the module's own default is 0 (red), so a specialization that did
// nothing paints the wrong colour.
const unsigned int fs = MakeSpirvShader(GL_FRAGMENT_SHADER, kFragmentModule,
sizeof(kFragmentModule) / sizeof(kFragmentModule[0]),
kChannelConstantId, 1u, &log);
ASSERT_NE(fs, 0u) << "fragment: " << log;
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked != GL_TRUE) {
GLint length = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
std::vector<char> programLog(static_cast<size_t>(length > 0 ? length : 1), '\0');
glGetProgramInfoLog(program, static_cast<GLsizei>(programLog.size()), nullptr, programLog.data());
FAIL() << "linking two specialized SPIR-V modules failed: " << programLog.data();
}
BindDefaultFramebuffer();
glViewport(0, 0, width, height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
GLuint vao = 0, vbo = 0;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glUseProgram(program);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
EXPECT_EQ(FirstGLError(), 0u);
const Image painted = ReadPixels(width, height);
const Rgba8 centre = painted.At(width / 2, height / 2);
EXPECT_LT(centre.r, 32) << "the fragment module wrote the wrong channel; constant id 7 was not applied";
EXPECT_GT(centre.g, 224) << "the centre of a 0.5-scaled quad must be painted";
// A pixel just inside the corner is OUTSIDE the 0.5-scaled quad and must still be the
// clear colour - which is what proves constant id 3 reached the vertex module. At the
// default scale of 1.0 the quad covers the whole viewport and this pixel would be green.
const Rgba8 corner = painted.At(1, 1);
EXPECT_LT(corner.g, 32) << "the quad was not scaled; the vertex specialization constant was not applied";
glBindVertexArray(0);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
glDeleteProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
gl.EndFrame();
}
} // namespace MGITest
+227
View File
@@ -4363,3 +4363,230 @@ TEST_F(ProgramTest, UniformEntryPointsRejectAnUnlinkedProgram) {
DrainProgramTestErrors();
}
// ---------------------------------------------------------------------------------------------
// GL_ARB_gl_spirv, core since 4.6. glShaderBinary and glSpecializeShader were
// DECLARE_GL_FUNCTION_STUB entry points - they took their arguments, recorded no error and did
// nothing - and glGetShaderiv(GL_SPIR_V_BINARY) fell into the terminal INVALID_ENUM arm, which is
// where all nine gl_spirv conformance bodies died.
//
// The module below is a real one, compiled ahead of time by glslangValidator (-G --target-env
// opengl) so this GPU-free binary needs no toolchain at run time. Its GLSL:
// layout(location = 0) in vec2 aPos;
// layout(constant_id = 3) const float uScale = 1.0;
// void main() { gl_Position = vec4(aPos * uScale, 0.0, 1.0); }
// The RENDERING half of the path is asserted separately, on a real context, in
// MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp.
// ---------------------------------------------------------------------------------------------
namespace {
// Asserts a call recorded exactly one error and drains it, so the next case starts clean.
void ExpectOnlyThisGlError(GLenum expected) {
EXPECT_EQ(GetError(), expected);
EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error";
}
// 255 words
const unsigned int kVertexModule[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000020u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu,
0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u,
0x0009000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000012u, 0x0000001eu,
0x0000001fu, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu,
0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u,
0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u,
0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, 0x00040005u, 0x00000012u, 0x736f5061u, 0x00000000u,
0x00040005u, 0x00000014u, 0x61635375u, 0x0000656cu, 0x00050005u, 0x0000001eu, 0x565f6c67u, 0x65747265u,
0x00444978u, 0x00060005u, 0x0000001fu, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x00000044u, 0x00030047u,
0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u,
0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu,
0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000012u,
0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x00000001u, 0x00000003u, 0x00040047u, 0x0000001eu,
0x0000000bu, 0x00000005u, 0x00040047u, 0x0000001fu, 0x0000000bu, 0x00000006u, 0x00020013u, 0x00000002u,
0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u,
0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u,
0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu, 0x0000000bu,
0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu,
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u,
0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u, 0x00000006u, 0x00000002u,
0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000001u,
0x00040032u, 0x00000006u, 0x00000014u, 0x3f800000u, 0x0004002bu, 0x00000006u, 0x00000016u, 0x00000000u,
0x0004002bu, 0x00000006u, 0x00000017u, 0x3f800000u, 0x00040020u, 0x0000001bu, 0x00000003u, 0x00000007u,
0x00040020u, 0x0000001du, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x0000001du, 0x0000001eu, 0x00000001u,
0x0004003bu, 0x0000001du, 0x0000001fu, 0x00000001u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000010u, 0x00000013u, 0x00000012u, 0x0005008eu,
0x00000010u, 0x00000015u, 0x00000013u, 0x00000014u, 0x00050051u, 0x00000006u, 0x00000018u, 0x00000015u,
0x00000000u, 0x00050051u, 0x00000006u, 0x00000019u, 0x00000015u, 0x00000001u, 0x00070050u, 0x00000007u,
0x0000001au, 0x00000018u, 0x00000019u, 0x00000016u, 0x00000017u, 0x00050041u, 0x0000001bu, 0x0000001cu,
0x0000000du, 0x0000000fu, 0x0003003eu, 0x0000001cu, 0x0000001au, 0x000100fdu, 0x00010038u,
};
} // namespace
TEST_F(ProgramTest, ShaderBinaryStoresASpirvModuleAndTheStateQueryReportsIt) {
DrainProgramTestErrors();
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
GLint isSpirv = GL_TRUE;
GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(GetError(), GL_NO_ERROR) << "GL_SPIR_V_BINARY is an accepted pname in a 4.6 context";
EXPECT_EQ(isSpirv, GL_FALSE);
ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
EXPECT_EQ(GetError(), GL_NO_ERROR);
GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(isSpirv, GL_TRUE);
// glCompileShader on a SPIR-V shader is INVALID_OPERATION: glSpecializeShader is what compiles
// one. This is the whole of spirv_modules_error_verification_test's first assertion.
CompileShader(shader);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
// glShaderSource takes the object back to being a GLSL shader.
const char* source = "#version 450 core\nvoid main() { gl_Position = vec4(0.0); }\n";
ShaderSource(shader, 1, &source, nullptr);
GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(isSpirv, GL_FALSE);
CompileShader(shader);
EXPECT_EQ(GetError(), GL_NO_ERROR);
DrainProgramTestErrors();
}
TEST_F(ProgramTest, ShaderBinaryValidatesItsArguments) {
DrainProgramTestErrors();
GLuint shaders[2] = {0, 0};
shaders[0] = CreateShader(GL_VERTEX_SHADER);
shaders[1] = CreateShader(GL_FRAGMENT_SHADER);
// The only accepted format is the SPIR-V one; the stub used to accept everything silently.
ShaderBinary(1, shaders, GL_PROGRAM_BINARY_FORMATS, kVertexModule, sizeof(kVertexModule));
ExpectOnlyThisGlError(GL_INVALID_ENUM);
// A SPIR-V module is a sequence of 32-bit words.
ShaderBinary(1, shaders, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule) - 1);
ExpectOnlyThisGlError(GL_INVALID_VALUE);
// The same shader twice is INVALID_VALUE, and nothing may have been attached.
GLuint duplicated[2] = {shaders[0], shaders[0]};
ShaderBinary(2, duplicated, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
ExpectOnlyThisGlError(GL_INVALID_VALUE);
GLint isSpirv = GL_TRUE;
GetShaderiv(shaders[0], GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(isSpirv, GL_FALSE) << "a rejected glShaderBinary is all-or-nothing";
// A name that is not a shader object.
GLuint bogus = 0xBADBEEF;
ShaderBinary(1, &bogus, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
ExpectOnlyThisGlError(GL_INVALID_VALUE);
// Something that is not SPIR-V at all: the magic number gate, before SPIRV-Cross ever sees it.
const unsigned int notSpirv[4] = {0xDEADBEEFu, 0u, 0u, 0u};
ShaderBinary(1, shaders, GL_SHADER_BINARY_FORMAT_SPIR_V, notSpirv, sizeof(notSpirv));
ExpectOnlyThisGlError(GL_INVALID_VALUE);
GetShaderiv(shaders[0], GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(isSpirv, GL_FALSE);
// ONE call, TWO shader objects - the shape spirv_modules_shader_binary_multiple_shader_objects_test
// exercises. Both end up holding the module.
ShaderBinary(2, shaders, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
EXPECT_EQ(GetError(), GL_NO_ERROR);
for (const GLuint shader : shaders) {
GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(isSpirv, GL_TRUE);
}
DrainProgramTestErrors();
}
TEST_F(ProgramTest, SpecializeShaderCompilesTheModuleAndAppliesItsConstants) {
DrainProgramTestErrors();
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
// Before any module: INVALID_OPERATION rather than a silent no-op.
const unsigned int constantId = 3;
const unsigned int constantValue = 0;
SpecializeShader(shader, "main", 1, &constantId, &constantValue);
ExpectOnlyThisGlError(GL_INVALID_OPERATION);
ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
ASSERT_EQ(GetError(), GL_NO_ERROR);
// Constant id 3 is the module's `uScale`, handed over as the bit pattern of 0.5f.
float half = 0.5f;
unsigned int halfBits = 0;
std::memcpy(&halfBits, &half, sizeof(halfBits));
SpecializeShader(shader, "main", 1, &constantId, &halfBits);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GLint compiled = GL_FALSE;
GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
char infoLog[2048] = "";
GetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(compiled, GL_TRUE) << infoLog;
// The module stays attached after specialization: glSpecializeShader may legally run again
// with different constants, and it has to re-specialize the ORIGINAL words.
GLint isSpirv = GL_FALSE;
GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv);
EXPECT_EQ(isSpirv, GL_TRUE);
SpecializeShader(shader, "main", 0, nullptr, nullptr);
EXPECT_EQ(GetError(), GL_NO_ERROR);
GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_TRUE) << "a second specialization of the same module must also compile";
DrainProgramTestErrors();
}
TEST_F(ProgramTest, SpecializeShaderReportsBadEntryPointsAndUnknownConstantsThroughCompileStatus) {
DrainProgramTestErrors();
const GLuint shader = CreateShader(GL_VERTEX_SHADER);
ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule));
ASSERT_EQ(GetError(), GL_NO_ERROR);
// A constant id the module does not declare. ARB_gl_spirv routes a failed specialization
// through COMPILE_STATUS and the info log, exactly as glCompileShader does - it is not a GL
// error, which is why an application that only checks glGetError would see nothing.
const unsigned int unknownId = 4242;
const unsigned int value = 0;
SpecializeShader(shader, "main", 1, &unknownId, &value);
GLint compiled = GL_TRUE;
GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_FALSE);
GLint logLength = 0;
GetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
EXPECT_GT(logLength, 0) << "a failed specialization has to say why";
// A repeated constant index is GL_INVALID_VALUE at the entry point itself.
const unsigned int repeated[2] = {3, 3};
const unsigned int values[2] = {0, 0};
SpecializeShader(shader, "main", 2, repeated, values);
ExpectOnlyThisGlError(GL_INVALID_VALUE);
// An entry point the module does not carry.
SpecializeShader(shader, "notMain", 0, nullptr, nullptr);
GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
EXPECT_EQ(compiled, GL_FALSE);
DrainProgramTestErrors();
}
TEST_F(ProgramTest, ShaderBinaryFormatsAreAdvertisedConsistently) {
DrainProgramTestErrors();
GLint count = -1;
GetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &count);
EXPECT_EQ(GetError(), GL_NO_ERROR);
ASSERT_EQ(count, 1);
GLint formats[4] = {0, 0, 0, 0};
GetIntegerv(GL_SHADER_BINARY_FORMATS, formats);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(formats[0], static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V))
<< "the count and the list have to describe the same thing";
DrainProgramTestErrors();
}