[Feat] (Program): implement GL_ARB_gl_spirv - glShaderBinary, glSpecializeShader and the SPIR_V_BINARY state, feeding the module into the ordinary compile pipeline

This commit is contained in:
2026-08-27 05:37:17 -04:00
parent be7bf21eb8
commit 6f299372c6
11 changed files with 517 additions and 12 deletions
@@ -160,7 +160,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, ReleaseShaderCompiler) DECLARE_GL_FUNCTION_S
DECLARE_GL_FUNCTION_HEAD(void, RenderbufferStorage, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, RenderbufferStorage, target, internalformat, width, height)
DECLARE_GL_FUNCTION_HEAD(void, SampleCoverage, GLfloat value, GLboolean invert) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SampleCoverage, value, invert)
DECLARE_GL_FUNCTION_HEAD(void, Scissor, GLint x, GLint y, GLsizei width, GLsizei height) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Scissor, x, y, width, height)
DECLARE_GL_FUNCTION_STUB_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
DECLARE_GL_FUNCTION_HEAD(void, ShaderBinary, GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderBinary, count, shaders, binaryformat, binary, length)
DECLARE_GL_FUNCTION_HEAD(void, ShaderSource, GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length) DECLARE_GL_FUNCTION_END_NO_RETURN(void, ShaderSource, shader, count, string, length)
DECLARE_GL_FUNCTION_HEAD(void, StencilFunc, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFunc, func, ref, mask)
DECLARE_GL_FUNCTION_HEAD(void, StencilFuncSeparate, GLenum face, GLenum func, GLint ref, GLuint mask) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilFuncSeparate, face, func, ref, mask)
@@ -1108,7 +1108,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnSeparableFilter, GLenum target, GLenum f
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnHistogram, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnHistogram, target, reset, format, type, bufSize, values)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmax, GLenum target, GLboolean reset, GLenum format, GLenum type, GLsizei bufSize, void* values) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetnMinmax, target, reset, format, type, bufSize, values)
DECLARE_GL_FUNCTION_HEAD(void, TextureBarrier, void) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TextureBarrier, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShader, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawArraysIndirectCount, GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawArraysIndirectCount, mode, indirect, drawcount, maxdrawcount, stride)
DECLARE_GL_FUNCTION_HEAD(void, MultiDrawElementsIndirectCount, GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) DECLARE_GL_FUNCTION_END_NO_RETURN(void, MultiDrawElementsIndirectCount, mode, type, indirect, drawcount, maxdrawcount, stride)
DECLARE_GL_FUNCTION_HEAD(void, PolygonOffsetClamp, GLfloat factor, GLfloat units, GLfloat clamp) DECLARE_GL_FUNCTION_END_NO_RETURN(void, PolygonOffsetClamp, factor, units, clamp)
@@ -1150,7 +1150,7 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterdvARB, GLenum target
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramLocalParameterfvARB, GLenum target, GLuint index, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramLocalParameterfvARB, target, index, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramStringARB, GLenum target, GLenum pname, void* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramStringARB, target, pname, string)
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferTextureFaceARB, GLenum target, GLenum attachment, GLuint texture, GLint level, GLenum face) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferTextureFaceARB, target, attachment, texture, level, face)
DECLARE_GL_FUNCTION_STUB_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, SpecializeShaderARB, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_HEAD(void, SpecializeShaderARB, GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants, const GLuint* pConstantIndex, const GLuint* pConstantValue) DECLARE_GL_FUNCTION_END_NO_RETURN(void, SpecializeShader, shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform1i64ARB, GLint location, GLint64 x) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform1i64ARB, location, x)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform2i64ARB, GLint location, GLint64 x, GLint64 y) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform2i64ARB, location, x, y)
DECLARE_GL_FUNCTION_STUB_HEAD(void, Uniform3i64ARB, GLint location, GLint64 x, GLint64 y, GLint64 z) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, Uniform3i64ARB, location, x, y, z)
+6 -1
View File
@@ -1990,8 +1990,13 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_NUM_SPIR_V_EXTENSIONS:
*params = 0;
return;
// GL_ARB_gl_spirv, core since 4.6: exactly one shader binary format, and the pair has to
// agree - an application sizes its GL_SHADER_BINARY_FORMATS array from the count.
case GL_NUM_SHADER_BINARY_FORMATS:
*params = 0; // ShaderBinary entrypoints are stubbed
*params = 1;
return;
case GL_SHADER_BINARY_FORMATS:
*params = static_cast<GLint>(GL_SHADER_BINARY_FORMAT_SPIR_V);
return;
case GL_PACK_ALIGNMENT:
*params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackAlignment);
@@ -11,6 +11,8 @@
#include "Config.h"
#include <cmath>
#include <limits>
#include <set>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Impl/GLImpl/VertexArray/Validators.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
@@ -331,9 +333,164 @@ namespace MobileGL::MG_Impl::GLImpl {
void CompileShader_State(GLuint shader) {
auto& shaderObject = TryToGetShaderObject(shader);
if (!shaderObject) return;
// ARB_gl_spirv: "INVALID_OPERATION is generated by CompileShader if shader has been
// associated with a SPIR-V binary". Such an object has no GLSL source to compile - it is
// waiting for glSpecializeShader, which is the operation that compiles it.
if (shaderObject->HasSpirvBinary()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"shader " + std::to_string(shader) +
" holds a SPIR-V binary; use glSpecializeShader instead of glCompileShader."));
return;
}
shaderObject->Compile();
}
// ---------------------------------------------------------------------------------------
// GL_ARB_gl_spirv
// ---------------------------------------------------------------------------------------
void ShaderBinary_State(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary,
GLsizei length) {
if (count < 0 || length < 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "count and length must be non-negative."));
return;
}
// GL_NUM_SHADER_BINARY_FORMATS advertises exactly one format, so every other value is
// INVALID_ENUM (GL 4.6 core 7.2). This is the check that used to be missing entirely -
// the entry point was a silent stub, so an application handed a format nothing supports
// and was told nothing.
if (binaryformat != GL_SHADER_BINARY_FORMAT_SPIR_V) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"binaryformat must be GL_SHADER_BINARY_FORMAT_SPIR_V."));
return;
}
if (count == 0) return;
if (shaders == nullptr || (length > 0 && binary == nullptr)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "shaders and binary must not be null."));
return;
}
// A SPIR-V module is a sequence of 32-bit words, so a length that is not a multiple of
// four cannot be one (ARB_gl_spirv makes this INVALID_VALUE).
if ((length % 4) != 0) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"length must be a multiple of four for a SPIR-V module."));
return;
}
// EVERY name is validated before ANY of them is written: the entry point is all-or-
// nothing, and half-applying it would leave some objects holding a module the call was
// rejected for. The duplicate check is the extension's own ("INVALID_VALUE ... if the
// same shader object is specified more than once").
std::set<GLuint> seen;
for (GLsizei i = 0; i < count; ++i) {
if (!seen.insert(shaders[i]).second) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"shader " + std::to_string(shaders[i]) +
" appears more than once in `shaders`."));
return;
}
if (!MG_State::pGLContext->ValidateShaderName(shaders[i])) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
std::to_string(shaders[i]) + " is not the name of a shader object."));
return;
}
}
const SizeT wordCount = static_cast<SizeT>(length) / 4;
Vector<Uint32> module(wordCount);
if (wordCount != 0) {
Memcpy(module.data(), binary, static_cast<SizeT>(length));
}
// spirv-val here, not at glSpecializeShader: this is where the words arrive, and past it
// they reach SPIRV-Cross, which parses rather than validates. ARB_gl_spirv lets an
// implementation reject an invalid module at either call; rejecting at the earlier one
// means the application's error is reported next to the data that caused it.
if (const auto validated = MG_Util::ShaderTranspiler::ShaderCompiler::ValidateSpirvModule(module);
!validated) {
MGLOG_D("%s: rejected SPIR-V module: %s", __func__, validated.error().log.c_str());
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, validated.error().log));
return;
}
for (GLsizei i = 0; i < count; ++i) {
auto& shaderObject = TryToGetShaderObject(shaders[i]);
if (!shaderObject) continue;
// A copy per object, not a shared buffer: each shader object may be specialized with
// different constants, and each specialization re-reads its own original words.
Vector<Uint32> perObject = module;
shaderObject->SetSpirvBinary(Move(perObject));
}
}
void SpecializeShader_State(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
auto& shaderObject = TryToGetShaderObject(shader);
if (!shaderObject) return;
if (!shaderObject->HasSpirvBinary()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"shader " + std::to_string(shader) +
" has no SPIR-V binary; call glShaderBinary first."));
return;
}
if (numSpecializationConstants > 0 && (pConstantIndex == nullptr || pConstantValue == nullptr)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"pConstantIndex and pConstantValue must not be null."));
return;
}
// "INVALID_VALUE is generated if any value in pConstantIndex is repeated" - checked before
// anything is applied, for the same all-or-nothing reason glShaderBinary checks its names
// up front.
Vector<Uint32> constantIds(pConstantIndex, pConstantIndex + numSpecializationConstants);
Vector<Uint32> constantValues(pConstantValue, pConstantValue + numSpecializationConstants);
{
std::set<Uint32> seen;
for (const Uint32 id : constantIds) {
if (seen.insert(id).second) continue;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"constant index " + std::to_string(id) + " is repeated."));
return;
}
}
const String entryPoint = pEntryPoint ? String(pEntryPoint) : String{};
const GLenum shaderType = MG_Util::ConvertShaderStageToGLEnum(shaderObject->GetShaderStage());
auto specialized = MG_Util::ShaderTranspiler::ShaderCompiler::SpecializeAndDecompileSpirvModule(
shaderObject->GetSpirvBinary(), shaderType, entryPoint, constantIds, constantValues);
if (!specialized) {
// Specialization failure is a COMPILE failure, not a GL error: ARB_gl_spirv routes it
// through COMPILE_STATUS and the info log exactly as glCompileShader does, so an
// application that checks the status the usual way sees it.
MGLOG_D("%s: specialization failed for shader %u: %s", __func__, shader,
specialized.error().log.c_str());
shaderObject->RecordSpecializationFailure(String(specialized.error().log));
return;
}
shaderObject->SpecializeFromSpirv(Move(specialized.value()));
}
// glMaxShaderCompilerThreadsKHR / glMaxShaderCompilerThreadsARB - one implementation,
// because GL_KHR_parallel_shader_compile and GL_ARB_parallel_shader_compile define the
// same entry point with the same semantics and GetProcAddress.cpp maps both spellings.
@@ -903,6 +1060,13 @@ namespace MobileGL::MG_Impl::GLImpl {
case GL_SHADER_SOURCE_LENGTH:
*params = shaderObject->GetShaderSource().empty() ? 0 : (GLint)shaderObject->GetShaderSource().length() + 1;
break;
// GL_ARB_gl_spirv. GL_SPIR_V_BINARY and GL_SPIR_V_BINARY_ARB are the same token: TRUE
// while the object stands for an application-supplied module. It is the FIRST thing the
// conformance suite asks after glShaderBinary, and it used to fall into the terminal
// default arm below and take the whole test with it.
case GL_SPIR_V_BINARY:
*params = shaderObject->HasSpirvBinary() ? GL_TRUE : GL_FALSE;
break;
// GL_KHR_parallel_shader_compile. THIS CASE MUST NOT JOIN - see the identical case in
// GetProgramiv_State. GL_COMPILE_STATUS two cases up deliberately DOES join (it has
// to: it reports the outcome); this one reports whether there is an outcome yet, and
@@ -2189,6 +2353,15 @@ namespace MobileGL::MG_Impl::GLImpl {
BindAttribLocation_State(program, index, name);
}
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length) {
ShaderBinary_State(count, shaders, binaryformat, binary, length);
}
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
const GLuint* pConstantIndex, const GLuint* pConstantValue) {
SpecializeShader_State(shader, pEntryPoint, numSpecializationConstants, pConstantIndex, pConstantValue);
}
void CompileShader(GLuint shader) {
CompileShader_State(shader);
}
@@ -13,6 +13,12 @@ namespace MobileGL::MG_Impl::GLImpl {
void AttachShader(GLuint program, GLuint shader);
void BindAttribLocation(GLuint program, GLuint index, const GLchar* name);
void CompileShader(GLuint shader);
// GL_ARB_gl_spirv, core since 4.6. The pair is a two-step operation: glShaderBinary attaches
// the module to one or more shader objects, glSpecializeShader names its entry point and
// supplies its specialization constants and is what actually compiles them.
void ShaderBinary(GLsizei count, const GLuint* shaders, GLenum binaryformat, const void* binary, GLsizei length);
void SpecializeShader(GLuint shader, const GLchar* pEntryPoint, GLuint numSpecializationConstants,
const GLuint* pConstantIndex, const GLuint* pConstantValue);
GLuint CreateProgram(void);
GLuint CreateShader(GLenum type);
void DeleteProgram(GLuint program);
@@ -15,7 +15,59 @@
#include <MG_Util/ShaderTranspiler/Types.h>
namespace MobileGL::MG_State::GLState {
void ShaderObject::SetSpirvBinary(Vector<Uint32>&& binary) {
// A module replaces whatever this object stood for, so the compiled state of the old
// source goes with it - including a compile still in flight.
ReleaseCompileNode();
m_spirvBinary = Move(binary);
m_hasSpirvBinary = true;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_source = MakeShared<const String>(String{});
InvalidateCompiledState();
}
void ShaderObject::SpecializeFromSpirv(String&& glsl) {
ReleaseCompileNode();
m_specializationFailed = false;
m_specializationInfoLog.clear();
// The GLSL the module specializes to enters the ORDINARY pipeline from here: preprocess,
// glslang parse, reflection, transpile, both backends. Nothing downstream needs to know
// the source was not written by the application - which is the whole reason this hop
// exists, and the reason a SPIR-V program's GL-visible surface (uniform locations, block
// indices, transform-feedback layout) is populated at all.
m_source = MakeShared<const String>(Move(glsl));
InvalidateCompiledState();
Compile();
}
void ShaderObject::RecordSpecializationFailure(String&& infoLog) {
ReleaseCompileNode();
m_source = MakeShared<const String>(String{});
InvalidateCompiledState();
m_specializationFailed = true;
m_specializationInfoLog = Move(infoLog);
}
void ShaderObject::SetShaderSource(const String& source) {
// glShaderSource on a SPIR-V shader takes the object back to being a GLSL one, and
// GL_SPIR_V_BINARY must then read FALSE (ARB_gl_spirv; gl4cGlSpirvTests'
// spirv_modules_state_queries_test checks exactly this transition). The stored module goes
// with the flag - re-specializing it would be re-specializing a shader the application has
// already replaced. The memo below is skipped on purpose: the source may well be
// byte-identical to the empty string this object has been holding, and keeping the
// "compiled state" of that would keep the module's verdict too.
if (m_hasSpirvBinary || m_specializationFailed) {
m_hasSpirvBinary = false;
m_spirvBinary.clear();
m_spirvBinary.shrink_to_fit();
m_specializationFailed = false;
m_specializationInfoLog.clear();
ReleaseCompileNode();
m_source = MakeShared<const String>(source);
InvalidateCompiledState();
return;
}
// P0b layer 1. glShaderSource always REPLACES the source, but replacing it with a
// byte-identical one cannot change what a compile would produce: the whole
// pipeline (preprocess -> lexical checks -> glslang parse) is a pure function of
@@ -36,6 +88,10 @@ namespace MobileGL::MG_State::GLState {
}
void ShaderObject::SetShaderSource(String&& source) {
if (m_hasSpirvBinary || m_specializationFailed) {
SetShaderSource(static_cast<const String&>(source));
return;
}
if (SourceMatchesCompiledState(source)) return;
ReleaseCompileNode();
m_source = MakeShared<const String>(Move(source));
@@ -65,6 +65,28 @@ namespace MobileGL {
void SetShaderSource(const String& source);
void SetShaderSource(String&& source);
void Compile();
// ---- GL_ARB_gl_spirv ----
// glShaderBinary(GL_SHADER_BINARY_FORMAT_SPIR_V): the object stops standing for a
// GLSL source and starts standing for an application-supplied SPIR-V module. The
// module is held verbatim until glSpecializeShader names an entry point for it -
// ARB_gl_spirv makes the pair a two-step operation, and glCompileShader in between is
// INVALID_OPERATION rather than a compile of anything.
//
// Both directions clear the other: glShaderSource on a SPIR-V shader takes it back to
// being a GLSL shader with GL_SPIR_V_BINARY reading FALSE, which the conformance suite
// checks explicitly.
void SetSpirvBinary(Vector<Uint32>&& binary);
Bool HasSpirvBinary() const { return m_hasSpirvBinary; }
const Vector<Uint32>& GetSpirvBinary() const { return m_spirvBinary; }
// glSpecializeShader's half: hand the object the GLSL its module specializes to and
// let the ordinary pipeline compile it.
void SpecializeFromSpirv(String&& glsl);
// The other half: specialization itself failed (a bad entry point, a constant id the
// module does not declare, a module spirv-val rejects). There is nothing to compile,
// so the verdict is recorded directly - COMPILE_STATUS false with this log - and both
// queries answer from it without touching the compile pipeline.
void RecordSpecializationFailure(String&& infoLog);
// Gives up this object's claim on its compile node, cancelling the node only if
// this object was its LAST claimant. Called at the points where the object's
// compiled state stops being observable through THIS name: a real source change,
@@ -99,14 +121,16 @@ namespace MobileGL {
const SharedPtr<const String>& GetShaderSourcePtr() const { return m_source; }
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
const String& GetInfoLog() const { return Compiled().infoLog; }
const String& GetInfoLog() const {
return m_specializationFailed ? m_specializationInfoLog : Compiled().infoLog;
}
// Explicit layout(location = N) qualifiers on this shader's default-block
// uniforms, as glslang recorded them at the point its Vulkan-relaxed remap
// discarded them (see CollectExplicitUniformLocations).
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
return Compiled().explicitUniformLocations;
}
Bool GetCompileStatus() const { return Compiled().compileStatus; }
Bool GetCompileStatus() const { return m_specializationFailed ? false : Compiled().compileStatus; }
Bool GetDeleteStatus() const { return m_deleteStatus; }
// Blocks until a pending compile has published its artifacts. Public for the
@@ -248,6 +272,18 @@ namespace MobileGL {
// query optimistically for the current node. Cleared wherever the node
// changes hands (AdoptCompileNode) or goes away (DropCompileNode).
mutable Bool m_optimisticAnswerLatched = false;
// The application-supplied SPIR-V module and the flag GL_SPIR_V_BINARY reports. The
// module is kept after specialization too: glSpecializeShader may legally run again on
// the same object with different constants, and the second call has to re-specialize
// the ORIGINAL words rather than the ones the first call folded.
Vector<Uint32> m_spirvBinary;
Bool m_hasSpirvBinary = false;
// A specialization that failed before any compile could start. Kept beside the
// compile artifacts rather than inside them because there is no compile job to hang
// it on - see RecordSpecializationFailure. Cleared by anything that gives the object
// a new meaning (a new source, a new module, a fresh specialization).
Bool m_specializationFailed = false;
String m_specializationInfoLog;
};
} // namespace MG_State::GLState
} // namespace MobileGL
@@ -244,11 +244,17 @@ TEST_F(ProgramPipelineCompositeTest, AValueIdenticalWriteStillTakesTheSlotForIts
DeleteProgramPipelines(1, &pipeline);
}
// glUseProgramStages here accepts a program that was never linked as separable (GL 4.6 core 7.4
// says it should not, and MobileGL validates only LINK_STATUS). Such a program has recorded
// none of its writes, because nothing ever armed its tracking latch - so the mirror has to fall
// back to carrying everything rather than carrying nothing. Mirroring nothing would have been a
// fresh regression on a shape that worked before the dirty set existed.
// A stage program that recorded NONE of its writes, because nothing ever armed its tracking
// latch: the mirror has to fall back to carrying everything rather than carrying nothing.
// Mirroring nothing would have been a fresh regression on a shape that worked before the dirty
// set existed.
//
// The shape used to be reachable through glUseProgramStages, which accepted a program that was
// never linked as separable. It no longer is: GL 4.6 core 7.4 requires the LATCHED
// PROGRAM_SEPARABLE flag and MobileGL now enforces it, and arming that flag is the very thing
// that arms the tracking latch - so no program the entry point accepts can be in this state. The
// fallback is therefore unreachable from GL and is exercised through the state layer instead,
// which is the only way left to keep it covered rather than deleting the coverage with the hole.
TEST_F(ProgramPipelineCompositeTest, ANonSeparableStageProgramStillMirrorsItsUniforms) {
const char* vsSource = R"(#version 430 core
uniform vec4 u_vsOnly;
@@ -271,9 +277,19 @@ void main() { gl_Position = u_vsOnly; }
GLuint pipeline = 0;
GenProgramPipelines(1, &pipeline);
BindProgramPipeline(pipeline);
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
// The fragment stage goes through the entry point; the vertex one cannot, so it is installed
// directly on the pipeline object - the same call glUseProgramStages makes once it is done
// validating, minus the validation this shape now fails.
UseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fs);
ASSERT_EQ(GetError(), GL_NO_ERROR);
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vs);
ASSERT_EQ(GetError(), GL_INVALID_OPERATION)
<< "a program not linked as separable is not a legal pipeline stage";
{
const auto& pipelineObject = MG_State::pGLContext->MaterializeProgramPipelineObject(pipeline);
ASSERT_NE(pipelineObject, nullptr);
pipelineObject->SetStageProgram(ShaderStage::Vertex, MG_State::pGLContext->GetProgramObject(vs));
}
const float written[4] = {3.0f, 1.0f, 4.0f, 1.0f};
ProgramUniform4fv(vs, GetUniformLocation(vs, "u_vsOnly"), 1, written);
@@ -1681,6 +1681,120 @@ namespace MobileGL {
return true;
}
namespace {
// The execution model an application-supplied module's entry point must carry for
// the shader object it was handed to. glShaderBinary attaches a module to a shader
// of a fixed type, and ARB_gl_spirv requires the specialized entry point to match.
SpvExecutionModel ExecutionModelForShaderType(GLenum shaderType) {
switch (shaderType) {
case GL_VERTEX_SHADER:
return SpvExecutionModelVertex;
case GL_TESS_CONTROL_SHADER:
return SpvExecutionModelTessellationControl;
case GL_TESS_EVALUATION_SHADER:
return SpvExecutionModelTessellationEvaluation;
case GL_GEOMETRY_SHADER:
return SpvExecutionModelGeometry;
case GL_COMPUTE_SHADER:
return SpvExecutionModelGLCompute;
case GL_FRAGMENT_SHADER:
default:
return SpvExecutionModelFragment;
}
}
} // namespace
Result<void> ShaderCompiler::ValidateSpirvModule(const Vector<Uint32>& spirv) {
ResultInfo r;
r.errc = -6;
if (spirv.size() < 5) {
r.log = "Error: [ARB_gl_spirv] the module is too short to be SPIR-V.";
return std::unexpected(r);
}
// 0x07230203 is SPIR-V's magic number. A module in the other byte order is a
// legal SPIR-V file but NOT one glShaderBinary accepts: ARB_gl_spirv fixes the
// word order to the host's.
if (spirv[0] != 0x07230203u) {
r.log = "Error: [ARB_gl_spirv] the module does not begin with the SPIR-V magic number.";
return std::unexpected(r);
}
PrepareSpirvValidation();
spvtools::SpirvTools tools(SPV_ENV_OPENGL_4_5);
String diagnostics;
tools.SetMessageConsumer([&diagnostics](spv_message_level_t, const char*, const spv_position_t&,
const char* message) {
if (!diagnostics.empty()) diagnostics += "\n";
diagnostics += message ? message : "";
});
if (!tools.Validate(spirv.data(), spirv.size())) {
r.log = "Error: [ARB_gl_spirv] the module failed SPIR-V validation:\n" + diagnostics;
return std::unexpected(r);
}
return {};
}
Result<String> ShaderCompiler::SpecializeAndDecompileSpirvModule(const Vector<Uint32>& spirv,
GLenum shaderType,
const String& entryPoint,
const Vector<Uint32>& constantIds,
const Vector<Uint32>& constantValues) {
SpvcSession session(spirv, SessionUsageBit::Transpile);
Uint32 unknownConstantId = 0;
if (!session.SetSpecializationConstants(constantIds, constantValues, unknownConstantId)) {
ResultInfo r;
r.errc = -7;
r.log = "Error: [ARB_gl_spirv] constant index " + std::to_string(unknownConstantId) +
" is not a specialization constant of this module.";
return std::unexpected(r);
}
if (!entryPoint.empty()) {
if (session.SetEntryPoint(entryPoint.c_str(), ExecutionModelForShaderType(shaderType)) !=
SPVC_SUCCESS) {
ResultInfo r;
r.errc = -8;
r.log = "Error: [ARB_gl_spirv] the module has no entry point named '" + entryPoint +
"' for this shader stage:\n" + String(session.GetLastErrorString());
return std::unexpected(r);
}
}
spvc_compiler_options options;
if (session.CreateOptions(&options) != SPVC_SUCCESS) {
ResultInfo r;
r.errc = -9;
r.log = "Error: [ARB_gl_spirv] could not create SPIRV-Cross options for the module.";
return std::unexpected(r);
}
// DESKTOP 4.60, not the ESSL 3.20 DecompileShader emits: this source goes back in
// at the FRONT of the pipeline, to be parsed by glslang exactly like an
// application's own GLSL, and every one of MobileGL's source-level passes is
// written against the desktop dialect. The ESSL hop happens later and unchanged,
// out of the SPIR-V this re-parse produces.
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);
// Vulkan semantics OFF is what makes this a GL source: descriptor sets collapse
// onto GL binding points, push constants become a uniform block, and - the point
// of the specialization pass above - every specialization constant is folded in
// as a literal instead of re-emitted as layout(constant_id = N).
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_SEPARATE_SHADER_OBJECTS, SPVC_TRUE);
session.SetOptions(options);
const char* emitted = nullptr;
session.Compile(&emitted);
if (!emitted) {
ResultInfo r;
r.errc = -10;
r.log = "Error: [ARB_gl_spirv] could not translate the module to GLSL:\n" +
String(session.GetLastErrorString());
return std::unexpected(r);
}
return String(emitted);
}
Result<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
spvc_compiler_options options;
session.CreateOptions(&options);
@@ -405,6 +405,45 @@ namespace MobileGL {
bool enableSpirvValidation = false);
static Result<String> DecompileShader(SpvcSession& session);
// ---- GL_ARB_gl_spirv ----
// Turn an APPLICATION-supplied SPIR-V module into the desktop GLSL the ordinary
// compile pipeline consumes.
//
// Why a round trip rather than handing the module straight to the backends. SPIR-V
// is not where MobileGL's pipeline STARTS: a program's whole GL-visible surface -
// every glGetActiveUniform, every uniform location, every block index, the
// transform-feedback layout, the default-block UBO routing - is reflected out of
// glslang's TProgram at link (ProgramLinkTask::SnapshotGlslangReflection), and
// glslang can only build one from a GLSL parse. Injecting the module at
// ProgramSpirvTask instead would skip the link entirely and leave every one of
// those queries answering nothing. Decompiling puts the application's module at
// the head of the SAME pipeline, so reflection, the relaxed default-block
// lowering, both backends and every memo tier work on it unchanged.
//
// What it costs, stated plainly: names. A module stripped of OpName (which
// ARB_gl_spirv permits, and the conformance suite deliberately does) comes back
// with SPIRV-Cross's generated identifiers rather than with none, so the
// *_MAX_LENGTH queries answer those instead of 1.
//
// `entryPoint` selects among several OpEntryPoint of this stage's execution
// model; an empty string means "whichever one is there". The specialization
// constants glSpecializeShader supplied are applied in the same pass - SPIRV-Cross
// folds each into the emitted source as a literal once Vulkan semantics are off,
// which is exactly what "specialize, then compile" means for a GLSL consumer.
//
// `constantIds` and `constantValues` are the parallel arrays the entry point
// takes. A constant id the module does not declare is GL_INVALID_VALUE per the
// extension; it is reported through the error log rather than silently ignored.
static Result<String> SpecializeAndDecompileSpirvModule(const Vector<Uint32>& spirv,
GLenum shaderType, const String& entryPoint,
const Vector<Uint32>& constantIds,
const Vector<Uint32>& constantValues);
// spirv-val over an application-supplied module, against the environment MobileGL
// parses and emits under. glShaderBinary is where a malformed module has to be
// caught: past it the words reach SPIRV-Cross, which is not a validator.
static Result<void> ValidateSpirvModule(const Vector<Uint32>& spirv);
// Parses one trivial shader in each configuration the production path can
// reach, on the calling thread, so the built-in symbol tables those
// configurations need are already cached before any worker asks for one.
@@ -496,6 +496,51 @@ namespace MobileGL {
SPVC_CHK_RETURN
}
spvc_result SpvcSession::SetEntryPoint(const char* name, SpvExecutionModel model) {
if (compiler == nullptr || name == nullptr || *name == '\0') return SPVC_SUCCESS;
return spvc_compiler_set_entry_point(compiler, name, model);
}
Bool SpvcSession::SetSpecializationConstants(const Vector<Uint32>& constantIds,
const Vector<Uint32>& constantValues,
Uint32& outUnknownConstantId) {
if (constantIds.empty()) return true;
if (compiler == nullptr) return false;
const spvc_specialization_constant* declared = nullptr;
SizeT declaredCount = 0;
if (spvc_compiler_get_specialization_constants(compiler, &declared, &declaredCount) != SPVC_SUCCESS) {
outUnknownConstantId = constantIds.front();
return false;
}
for (SizeT i = 0; i < constantIds.size(); ++i) {
const Uint32 wantedId = constantIds[i];
spvc_constant handle = nullptr;
for (SizeT j = 0; j < declaredCount; ++j) {
if (declared[j].constant_id != wantedId) continue;
handle = spvc_compiler_get_constant_handle(compiler, declared[j].id);
break;
}
if (handle == nullptr) {
// ARB_gl_spirv: "INVALID_VALUE is generated if any value in pConstantIndex
// refers to a specialization constant that does not exist in the shader
// module". Reported rather than skipped - a silently ignored id would let
// the shader specialize to something the application never asked for.
outUnknownConstantId = wantedId;
return false;
}
// The GL side hands over a flat GLuint per constant and ARB_gl_spirv says it
// is "interpreted according to the type of the specialization constant", so
// the 32-bit PATTERN is what has to be stored, not a converted number.
// spvc_constant_set_scalar_u32 writes exactly that pattern into the constant's
// scalar union, which SPIRV-Cross then reads back as whatever the constant's
// declared type is - the reinterpretation the extension asks for, for free.
spvc_constant_set_scalar_u32(handle, 0, 0, constantValues[i]);
}
return true;
}
spvc_result SpvcSession::Compile(const char** result) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT
@@ -160,6 +160,21 @@ namespace MobileGL {
// A block carrying only ONE of the two is left exactly as it is: those really do
// constrain the accesses the shader makes, and the driver is entitled to know.
spvc_result RelaxReadWriteExclusiveStorageBuffers();
// ---- GL_ARB_gl_spirv: an APPLICATION-supplied module, not one MobileGL emitted ----
// Select which OpEntryPoint of `model` this session compiles. A module may carry
// several of the same execution model, and glSpecializeShader names the one the
// shader object stands for.
spvc_result SetEntryPoint(const char* name, SpvExecutionModel model);
// Bake glSpecializeShader's values into the module's specialization constants.
// Every value is a GLuint on the GL side and is reinterpreted according to the
// constant's own scalar type, exactly as ARB_gl_spirv specifies ("the value is
// interpreted as the type of the specialization constant"). Returns false and
// sets `outUnknownConstantId` when an id the caller passed is not a
// specialization constant of this module, which the extension makes
// GL_INVALID_VALUE.
Bool SetSpecializationConstants(const Vector<Uint32>& constantIds,
const Vector<Uint32>& constantValues,
Uint32& outUnknownConstantId);
spvc_result Compile(const char** result);
const SpvcMetadata& GetMetadata() const;
const char* GetLastErrorString() const;