[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
@@ -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;