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