diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index eb2554df..ed94936a 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -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) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index 69a1dd49..211f7141 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -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(GL_SHADER_BINARY_FORMAT_SPIR_V); return; case GL_PACK_ALIGNMENT: *params = MG_State::pGLContext->GetPixelStoreParam(PixelStoreParam::PackAlignment); diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index 940bb189..f12f8ba0 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -11,6 +11,8 @@ #include "Config.h" #include #include +#include +#include #include #include #include @@ -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( + "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("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("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("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("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 seen; + for (GLsizei i = 0; i < count; ++i) { + if (!seen.insert(shaders[i]).second) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("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("MG_Impl/GLImpl", __func__, + std::to_string(shaders[i]) + " is not the name of a shader object.")); + return; + } + } + + const SizeT wordCount = static_cast(length) / 4; + Vector module(wordCount); + if (wordCount != 0) { + Memcpy(module.data(), binary, static_cast(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("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 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("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("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 constantIds(pConstantIndex, pConstantIndex + numSpecializationConstants); + Vector constantValues(pConstantValue, pConstantValue + numSpecializationConstants); + { + std::set seen; + for (const Uint32 id : constantIds) { + if (seen.insert(id).second) continue; + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("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); } diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h index cdebc445..5dd7bc8a 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.h @@ -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); diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp index 8158e0f7..bcf65d2e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp @@ -15,7 +15,59 @@ #include namespace MobileGL::MG_State::GLState { + void ShaderObject::SetSpirvBinary(Vector&& 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(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(Move(glsl)); + InvalidateCompiledState(); + Compile(); + } + + void ShaderObject::RecordSpecializationFailure(String&& infoLog) { + ReleaseCompileNode(); + m_source = MakeShared(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(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(source)); + return; + } if (SourceMatchesCompiledState(source)) return; ReleaseCompileNode(); m_source = MakeShared(Move(source)); diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h index fb2828fd..9dcf7023 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h @@ -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&& binary); + Bool HasSpirvBinary() const { return m_hasSpirvBinary; } + const Vector& 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& GetShaderSourcePtr() const { return m_source; } const SharedPtr& 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& 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 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 diff --git a/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp b/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp index daf0955e..94a9f6a5 100644 --- a/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramPipelineCompositeTest.cpp @@ -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); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 0b7ba2de..ac5aa4a1 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -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 ShaderCompiler::ValidateSpirvModule(const Vector& 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 ShaderCompiler::SpecializeAndDecompileSpirvModule(const Vector& spirv, + GLenum shaderType, + const String& entryPoint, + const Vector& constantIds, + const Vector& 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 ShaderCompiler::DecompileShader(SpvcSession& session) { spvc_compiler_options options; session.CreateOptions(&options); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 90c41c3c..3264c56f 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -405,6 +405,45 @@ namespace MobileGL { bool enableSpirvValidation = false); static Result 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 SpecializeAndDecompileSpirvModule(const Vector& spirv, + GLenum shaderType, const String& entryPoint, + const Vector& constantIds, + const Vector& 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 ValidateSpirvModule(const Vector& 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. diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp index 095149c9..622f92d7 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp @@ -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& constantIds, + const Vector& 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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h index c1d7114a..63516db0 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h @@ -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& constantIds, + const Vector& constantValues, + Uint32& outUnknownConstantId); spvc_result Compile(const char** result); const SpvcMetadata& GetMetadata() const; const char* GetLastErrorString() const;