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