mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[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:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user