mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Test] (GlSpirv): unit and headless-GPU coverage for glShaderBinary, glSpecializeShader and the SPIR_V_BINARY state
This commit is contained in:
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user