mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix, Test] (DirectGLES, ShaderTranspiler): synthesize the pass-through tessellation control stage ES requires
This commit is contained in:
@@ -2393,7 +2393,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// different one makes what was built wrong. Asked of the twin because only it
|
||||
// knows which units its own images address - and answered by an empty-vector
|
||||
// test for every program that declares its formats, which is nearly all of them.
|
||||
!twin->ImageUnitFormatsStillMatch()) {
|
||||
!twin->ImageUnitFormatsStillMatch() ||
|
||||
// A fifth of the same shape, for the programs ES will not link at all: one whose
|
||||
// tessellation evaluation stage has no control stage gets a synthesized
|
||||
// pass-through one, and GL_PATCH_VERTICES is compiled INTO it as
|
||||
// `layout(vertices = N) out` - so a glPatchParameteri between two draws makes the
|
||||
// built program wrong. -1 is "this program needed no such stage", which compares
|
||||
// equal to itself and costs every other program one integer test.
|
||||
(twin->GetPassthroughTessControlPatchVertices() >= 0 &&
|
||||
twin->GetPassthroughTessControlPatchVertices() !=
|
||||
static_cast<Int>(MG_State::pGLContext->GetPatchVertices()))) {
|
||||
twin->SyncToBackend(currentProgram);
|
||||
}
|
||||
g_currentDrawFrontendProgram = currentProgram.get();
|
||||
|
||||
@@ -5864,6 +5864,123 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
// GL 4.6 core 11.2.2 lets a program have a tessellation EVALUATION shader and no CONTROL
|
||||
// shader: the input patch is passed through unmodified and the levels come from the
|
||||
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state. OpenGL ES 3.2 has no such
|
||||
// state and no such allowance - it rejects the program at link, and with an EMPTY info
|
||||
// log, which was verified on an Adreno 830 with no MobileGL in the process (TES-only:
|
||||
// link=0, log empty; the same shaders plus any TCS: link=1, with or without the SSBO and
|
||||
// atomic counter the failing conformance case also declares). The frontend's own glslang
|
||||
// link succeeds, so GL_LINK_STATUS reads TRUE, program 0 is bound in its place, and every
|
||||
// draw silently renders nothing - a black framebuffer, an atomic counter still at 0 and
|
||||
// an untouched SSBO, with no error anywhere.
|
||||
//
|
||||
// So the missing stage is synthesized and attached here, alongside the program's own.
|
||||
// DirectVulkan already does exactly this for the same structural reason
|
||||
// (ProgramFactory::BuildPassthroughTessControlSource), so this completes the pair rather
|
||||
// than inventing an approach.
|
||||
//
|
||||
// Nothing that works today can be harmed by it: it fires ONLY for a program that has an
|
||||
// evaluation stage and no control stage, and every such program fails to link on ES right
|
||||
// now. The worst case is that the synthesized stage fails to compile or link, which leaves
|
||||
// the program exactly as dead as it already was - but with a driver log that says why,
|
||||
// where today there is an empty one.
|
||||
void BackendProgramObjectImpl::AttachPassthroughTessControlStage(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject, const Int tessEvalShaderIndex,
|
||||
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
|
||||
const String& tessEvalStageEssl) {
|
||||
// PATCH_VERTICES is dynamic state, and it decides the synthesized stage's output
|
||||
// patch size - so a program built for one value is stale for another. Recorded here
|
||||
// and compared on the draw path (SyncCurrentProgram), the same shape as the
|
||||
// storage-block and image-format signatures next to it.
|
||||
const Uint patchVertices = MG_State::pGLContext != nullptr
|
||||
? MG_State::pGLContext->GetPatchVertices()
|
||||
: 3u;
|
||||
m_passthroughTessControlPatchVertices = static_cast<Int>(patchVertices);
|
||||
|
||||
if (tessEvalShaderIndex < 0 ||
|
||||
static_cast<SizeT>(tessEvalShaderIndex) >= shaderSpirvs.size()) {
|
||||
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage, but no "
|
||||
"SPIR-V for it; the pass-through control stage GL describes cannot be checked, so "
|
||||
"the program is left to fail its ES link.",
|
||||
stateProgramObject.GetExternalIndex());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// The one shape the pass-through cannot stand in for. It forwards gl_Position and
|
||||
// nothing else, so an evaluation stage that reads a user-defined varying or a
|
||||
// per-patch input - both of which carry a Location, where every built-in it needs
|
||||
// does not - would start reading undefined values the moment a control stage sat
|
||||
// between it and the vertex stage. Declining keeps that from being silent; it is the
|
||||
// identical rule DirectVulkan applies in ReflectPassthroughTessControlNeed.
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::ModuleReadsLocatedInput(
|
||||
shaderSpirvs[static_cast<SizeT>(tessEvalShaderIndex)])) {
|
||||
MGLOG_E("Program %u has a tessellation evaluation stage with no control stage AND reads a "
|
||||
"user-defined input through it; a synthesized pass-through control stage cannot "
|
||||
"forward that, so the program is declined rather than fed an undefined varying.",
|
||||
stateProgramObject.GetExternalIndex());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Mirrored from the neighbours rather than fixed: whether SPIRV-Cross redeclares
|
||||
// gl_PerVertex, and with which members, depends on what the application's shaders
|
||||
// touched, and a synthesized stage that redeclares a DIFFERENT shape than the stage
|
||||
// it feeds is an ES link error against a program with no other problem. gl_in copies
|
||||
// the vertex stage's OUT block (that is what arrives) and gl_out the evaluation
|
||||
// stage's IN block (that is what is expected). A neighbour that redeclared nothing
|
||||
// yields an empty list, which leaves the driver's own built-in declaration in place -
|
||||
// which is exactly what matching it requires.
|
||||
const String inMembers =
|
||||
ExtractPerVertexBlockMembers(vertexStageEssl, /*input=*/false).value_or(String());
|
||||
const String outMembers =
|
||||
ExtractPerVertexBlockMembers(tessEvalStageEssl, /*input=*/true).value_or(String());
|
||||
|
||||
const String source =
|
||||
BuildPassthroughTessControlEssl(ResolveBackendEsslVersion(), patchVertices, inMembers, outMembers);
|
||||
|
||||
const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER);
|
||||
if (backendShaderId == 0) {
|
||||
MGLOG_E("Failed to create the synthesized pass-through tessellation control shader for "
|
||||
"program %u.",
|
||||
stateProgramObject.GetExternalIndex());
|
||||
m_backendProgramUsable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const char* sourceCStr = source.c_str();
|
||||
MGLOG_D("Synthesized pass-through tessellation control stage for program %u (patch vertices "
|
||||
"%u):\n%s",
|
||||
stateProgramObject.GetExternalIndex(), patchVertices, sourceCStr);
|
||||
g_GLESFuncs.glShaderSource(backendShaderId, 1, &sourceCStr, nullptr);
|
||||
g_GLESFuncs.glCompileShader(backendShaderId);
|
||||
|
||||
// GL_FALSE, not GL_TRUE, for the reason the per-stage loop states: an unwritten
|
||||
// out-param must read as "compile failed" and never as a silent success.
|
||||
GLint compileStatus = GL_FALSE;
|
||||
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_COMPILE_STATUS, &compileStatus);
|
||||
if (compileStatus == GL_FALSE) {
|
||||
GLint logLength = 0;
|
||||
g_GLESFuncs.glGetShaderiv(backendShaderId, GL_INFO_LOG_LENGTH, &logLength);
|
||||
if (logLength < 0) logLength = 0;
|
||||
Vector<GLchar> log(static_cast<SizeT>(logLength) + 1, '\0');
|
||||
g_GLESFuncs.glGetShaderInfoLog(backendShaderId, logLength, nullptr, log.data());
|
||||
log.back() = '\0';
|
||||
MGLOG_E("The synthesized pass-through tessellation control stage failed to compile for "
|
||||
"program %u. Driver log: %s\nSource:\n%s",
|
||||
stateProgramObject.GetExternalIndex(), log.data(), sourceCStr);
|
||||
m_backendProgramUsable = false;
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
return;
|
||||
}
|
||||
|
||||
g_GLESFuncs.glAttachShader(m_backendProgramId, backendShaderId);
|
||||
// Same ownership handover as every other stage: glDeleteShader only FLAGS, so this is
|
||||
// what makes the program own it and what keeps a relink from leaking it.
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
}
|
||||
|
||||
void BackendProgramObjectImpl::SyncToBackend(
|
||||
const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject) {
|
||||
#ifdef TRACY_ENABLE
|
||||
@@ -5909,6 +6026,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// reading it afterwards - resolves the same slot for the same GL binding.
|
||||
m_atomicCounterGlBindings.clear();
|
||||
m_atomicCounterEsslBindingTop = AtomicCounterEsslBindingTop();
|
||||
// Re-established by AttachPassthroughTessControlStage below when this program needs
|
||||
// one; cleared first so a program that stops needing one (a relink that now attaches
|
||||
// a real control stage) does not keep comparing against a stale patch size.
|
||||
m_passthroughTessControlPatchVertices = -1;
|
||||
// The same shape again for image FORMATS: what a format-less image declaration
|
||||
// compiles to depends on live glBindImageTexture state, so the pairs it was built
|
||||
// against are recorded here and compared per draw (ImageUnitFormatsStillMatch).
|
||||
@@ -6047,6 +6168,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return candidate;
|
||||
};
|
||||
|
||||
// Desktop GL makes the tessellation CONTROL stage optional; OpenGL ES 3.2 rejects a
|
||||
// program that has an evaluation stage without one, with an empty info log. When that
|
||||
// is this program's shape, one is synthesized below - and it has to be spelled to
|
||||
// MATCH the two stages it sits between, so their emitted ESSL is kept here as it is
|
||||
// produced. Empty for every program that has a control stage of its own, which is
|
||||
// all but a handful.
|
||||
Bool hasTessEvalStage = false;
|
||||
Bool hasTessControlStage = false;
|
||||
Int tessEvalShaderIndex = -1;
|
||||
String vertexStageEssl;
|
||||
String tessEvalStageEssl;
|
||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||
const auto stage = attachedShaders[index]->GetShaderStage();
|
||||
if (stage == ShaderStage::TessControl) hasTessControlStage = true;
|
||||
if (stage == ShaderStage::TessEval) {
|
||||
hasTessEvalStage = true;
|
||||
tessEvalShaderIndex = index;
|
||||
}
|
||||
}
|
||||
const Bool needsPassthroughTessControl = hasTessEvalStage && !hasTessControlStage;
|
||||
|
||||
for (int index = 0; index < attachedShaders.size(); ++index) {
|
||||
auto& shader = attachedShaders[index];
|
||||
GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage());
|
||||
@@ -6374,9 +6516,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// MobileGL creates without an owning wrapper to destroy it.
|
||||
g_GLESFuncs.glDeleteShader(backendShaderId);
|
||||
|
||||
// Kept AFTER every text-level pass, so what the synthesized control stage mirrors
|
||||
// is the text the driver actually sees, not an intermediate form.
|
||||
if (needsPassthroughTessControl) {
|
||||
if (glShaderType == GL_VERTEX_SHADER) {
|
||||
vertexStageEssl = source;
|
||||
} else if (glShaderType == GL_TESS_EVALUATION_SHADER) {
|
||||
tessEvalStageEssl = source;
|
||||
}
|
||||
}
|
||||
|
||||
MGLOG_D("Processed shader source length: %zu", source.length());
|
||||
}
|
||||
|
||||
if (needsPassthroughTessControl) {
|
||||
AttachPassthroughTessControlStage(*stateProgramObject, tessEvalShaderIndex, shaderSpirvs,
|
||||
vertexStageEssl, tessEvalStageEssl);
|
||||
}
|
||||
|
||||
// A counter buffer declared by several stages was recorded once per stage; the draw
|
||||
// path binds per GL binding point, so collapse the duplicates here rather than
|
||||
// re-issuing the same glBindBufferBase two or three times every draw.
|
||||
|
||||
@@ -1222,6 +1222,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// counter sync at one empty-vector test.
|
||||
const Vector<Int>& GetAtomicCounterBindings() const { return m_atomicCounterGlBindings; }
|
||||
Int GetAtomicCounterEsslBindingTop() const { return m_atomicCounterEsslBindingTop; }
|
||||
// GL_PATCH_VERTICES the synthesized pass-through tessellation control stage was built
|
||||
// for, or -1 when this program needed no such stage. Another of the same shape as the
|
||||
// signatures above: the value is compiled INTO the synthesized stage as
|
||||
// `layout(vertices = N) out`, so a program built for one patch size is stale for
|
||||
// another and the draw path has to say so. -1 compares equal to itself for every
|
||||
// program that has a control stage of its own, i.e. for all but a handful.
|
||||
Int GetPassthroughTessControlPatchVertices() const {
|
||||
return m_passthroughTessControlPatchVertices;
|
||||
}
|
||||
|
||||
Bool HasGlobalUboBlock() const { return m_globalUboBackendBlockIndex >= 0; }
|
||||
const Vector<Int>& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; }
|
||||
@@ -1270,6 +1279,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
void CacheResourceLocations(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
||||
|
||||
// Builds, compiles and attaches the pass-through tessellation control stage GL 4.6
|
||||
// core 11.2.2 describes, for a program that has an evaluation stage and none of its
|
||||
// own - which ES 3.2 rejects outright. Called from SyncToBackend after every real
|
||||
// stage has been attached and before the link; see the definition for why it cannot
|
||||
// regress a program that works today.
|
||||
void AttachPassthroughTessControlStage(
|
||||
const MG_State::GLState::ProgramObject& stateProgramObject, Int tessEvalShaderIndex,
|
||||
const Vector<Vector<unsigned int>>& shaderSpirvs, const String& vertexStageEssl,
|
||||
const String& tessEvalStageEssl);
|
||||
|
||||
// One stage's SPIR-V through the DirectGLES pass chain and SPIRV-Cross, producing
|
||||
// the raw emitted ESSL and the interface blocks this stage's XFB flattening
|
||||
// rewrote. This is the segment the L2 shader-translation memo keys on, so every
|
||||
@@ -1307,6 +1326,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint64 m_shaderStorageBlockBindingSignature = 0;
|
||||
Vector<Int> m_atomicCounterGlBindings;
|
||||
Int m_atomicCounterEsslBindingTop = -1;
|
||||
// -1 for every program that has a tessellation control stage of its own (or none at
|
||||
// all); otherwise the GL_PATCH_VERTICES the synthesized pass-through stage was built
|
||||
// with. See GetPassthroughTessControlPatchVertices.
|
||||
Int m_passthroughTessControlPatchVertices = -1;
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_backendProgramUsable = false;
|
||||
|
||||
|
||||
@@ -746,6 +746,82 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, const Bool input) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Deliberately a scan for the DECLARATION rather than a regex over the whole text:
|
||||
// "gl_PerVertex" also appears inside the block's own body in some emissions, and the
|
||||
// direction keyword has to be the one immediately preceding the name for the match to
|
||||
// mean what this needs it to mean.
|
||||
const auto isIdentifierChar = [](char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '_';
|
||||
};
|
||||
const String keyword = input ? String("in") : String("out");
|
||||
SizeT pos = 0;
|
||||
while ((pos = essl.find("gl_PerVertex", pos)) != String::npos) {
|
||||
// Walk back over whitespace to the direction keyword.
|
||||
SizeT before = pos;
|
||||
while (before > 0 && std::isspace(static_cast<unsigned char>(essl[before - 1]))) --before;
|
||||
const Bool matches = before >= keyword.size() &&
|
||||
essl.compare(before - keyword.size(), keyword.size(), keyword) == 0 &&
|
||||
(before == keyword.size() ||
|
||||
!isIdentifierChar(essl[before - keyword.size() - 1]));
|
||||
if (!matches) {
|
||||
pos += 1;
|
||||
continue;
|
||||
}
|
||||
const SizeT open = essl.find('{', pos);
|
||||
if (open == String::npos) return std::nullopt;
|
||||
const SizeT close = essl.find('}', open);
|
||||
if (close == String::npos) return std::nullopt;
|
||||
return essl.substr(open + 1, close - open - 1);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers) {
|
||||
#ifdef TRACY_ENABLE
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// Tessellation is core in ES 3.2 and reachable in 3.1 only through
|
||||
// GL_EXT_tessellation_shader. The caller has already established that the driver runs
|
||||
// the evaluation stage at all, so the only question here is which spelling to use.
|
||||
const Bool core = esslVersion >= 320;
|
||||
String source = "#version " + std::to_string(core ? 320u : 310u) + " es\n";
|
||||
if (!core) {
|
||||
source += "#extension GL_EXT_tessellation_shader : require\n";
|
||||
}
|
||||
source += "precision highp float;\n";
|
||||
source += "precision highp int;\n";
|
||||
source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n";
|
||||
// Mirrored, never invented. An empty member list means the neighbouring stage did not
|
||||
// redeclare the block either, and the driver's own built-in declaration is then what
|
||||
// both sides agree on - redeclaring here would be the thing that broke the match.
|
||||
if (!inPerVertexMembers.empty()) {
|
||||
source += "in gl_PerVertex {" + inPerVertexMembers + "} gl_in[gl_MaxPatchVertices];\n";
|
||||
}
|
||||
if (!outPerVertexMembers.empty()) {
|
||||
source += "out gl_PerVertex {" + outPerVertexMembers + "} gl_out[];\n";
|
||||
}
|
||||
source += "void main() {\n";
|
||||
// Only gl_Position is forwarded. That is the whole of what the pass-through owes the
|
||||
// evaluation stage: a program whose evaluation stage reads anything else per-vertex
|
||||
// was declined before this was ever called (ModuleReadsLocatedInput), and gl_PointSize
|
||||
// from a tessellation stage is a separate capability on both targets.
|
||||
source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n";
|
||||
source += " gl_TessLevelOuter[0] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[1] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[2] = 1.0;\n";
|
||||
source += " gl_TessLevelOuter[3] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[0] = 1.0;\n";
|
||||
source += " gl_TessLevelInner[1] = 1.0;\n";
|
||||
source += "}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool IsImagePassIdentifierChar(char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
|
||||
|
||||
@@ -280,6 +280,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// it reads need no entry in BuildEsslTranslationKey.
|
||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||
Int stageImageUniformBudget, Vector<String>* outDeclined = nullptr);
|
||||
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
|
||||
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
|
||||
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
|
||||
// `out` one.
|
||||
//
|
||||
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
|
||||
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
|
||||
// which members, depends on what the application's shader touched; a synthesized stage
|
||||
// that redeclares a different shape than its neighbours is an ES link error against a
|
||||
// program that has no other problem.
|
||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
|
||||
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
|
||||
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
//
|
||||
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
|
||||
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
|
||||
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
|
||||
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
|
||||
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
|
||||
// bound in its place, and every draw silently renders nothing.
|
||||
//
|
||||
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
|
||||
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
|
||||
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
|
||||
// alone, which is what matching a neighbour that did not redeclare requires.
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain. They are literal 1.0 because that is the GL
|
||||
// default and glPatchParameterfv - their only setter - is a stub in this frontend
|
||||
// (MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means making
|
||||
// the levels a parameter here AND part of what makes a built program stale, exactly as
|
||||
// PATCH_VERTICES already is; the two must move together, so they are named together.
|
||||
//
|
||||
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
||||
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
||||
// tessellation stages. Kept as two generators rather than one because the two targets
|
||||
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
|
||||
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
|
||||
// VkShaderModule against a driver shader object.
|
||||
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already stage-tagged) name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BakeImageFormatQualifiers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BuildPassthroughTessControlEssl;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ExtractPerVertexBlockMembers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_STAGE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_UNIT_MAP_PREFIX;
|
||||
@@ -1015,3 +1017,84 @@ void main() { gl_ViewportIndex = 1; imageStore(uni_image, ivec2(0), uvec4(1u));
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_NV_image_formats : require\n")) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_OES_viewport_array : require\n")) << out;
|
||||
}
|
||||
|
||||
// --- pass-through tessellation control stage --------------------------------------------------
|
||||
//
|
||||
// Desktop GL makes the tessellation control stage optional and takes the levels from
|
||||
// PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL; ES 3.2 has neither, and rejects a
|
||||
// program that has an evaluation stage without a control stage - with an EMPTY info log. The
|
||||
// synthesized stage is what stands in, and it has to MIRROR its two neighbours' gl_PerVertex
|
||||
// rather than pick a shape, because a redeclaration that disagrees with the stage it feeds is an
|
||||
// ES link error against a program that has nothing else wrong with it.
|
||||
|
||||
TEST(PassthroughTessControlEsslTest, DeclaresThePatchSizeAndWritesEveryTessLevel) {
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, "", "");
|
||||
EXPECT_EQ(out.find("#version 320 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "layout(vertices = 4) out;")) << out;
|
||||
EXPECT_TRUE(Contains(out,
|
||||
"gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;"))
|
||||
<< out;
|
||||
// All six, unconditionally: writing a level the evaluation stage's domain does not use is
|
||||
// legal and ignored, and it saves the generator from having to know the domain.
|
||||
for (const char* level : {"gl_TessLevelOuter[0]", "gl_TessLevelOuter[1]", "gl_TessLevelOuter[2]",
|
||||
"gl_TessLevelOuter[3]", "gl_TessLevelInner[0]", "gl_TessLevelInner[1]"}) {
|
||||
EXPECT_TRUE(Contains(out, String(level) + " = 1.0;")) << level << "\n" << out;
|
||||
}
|
||||
// Nothing redeclared when the neighbours redeclared nothing - the driver's own built-in
|
||||
// gl_in/gl_out is then what both sides agree on, and redeclaring is what would break it.
|
||||
EXPECT_FALSE(Contains(out, "gl_PerVertex")) << out;
|
||||
}
|
||||
|
||||
// ES 3.1 reaches tessellation only through the extension; the caller has already established
|
||||
// that the driver runs the evaluation stage at all, so the only question is the spelling.
|
||||
TEST(PassthroughTessControlEsslTest, RequestsTheExtensionBelowEs32) {
|
||||
const String out = BuildPassthroughTessControlEssl(310, 3, "", "");
|
||||
EXPECT_EQ(out.find("#version 310 es"), 0u) << out;
|
||||
EXPECT_TRUE(Contains(out, "#extension GL_EXT_tessellation_shader : require")) << out;
|
||||
}
|
||||
|
||||
TEST(PassthroughTessControlEsslTest, MirrorsTheNeighboursPerVertexBlocks) {
|
||||
const String inMembers = " highp vec4 gl_Position; highp float gl_PointSize; ";
|
||||
const String outMembers = " highp vec4 gl_Position; ";
|
||||
const String out = BuildPassthroughTessControlEssl(320, 4, inMembers, outMembers);
|
||||
EXPECT_TRUE(Contains(out, "in gl_PerVertex {" + inMembers + "} gl_in[gl_MaxPatchVertices];")) << out;
|
||||
EXPECT_TRUE(Contains(out, "out gl_PerVertex {" + outMembers + "} gl_out[];")) << out;
|
||||
}
|
||||
|
||||
TEST(ExtractPerVertexBlockMembersTest, ReadsEitherDirectionAndOnlyThatDirection) {
|
||||
const String essl = R"(#version 320 es
|
||||
in gl_PerVertex { highp vec4 gl_Position; } gl_in[gl_MaxPatchVertices];
|
||||
out gl_PerVertex { highp vec4 gl_Position; highp float gl_PointSize; } gl_out[];
|
||||
void main() {}
|
||||
)";
|
||||
const auto inMembers = ExtractPerVertexBlockMembers(essl, true);
|
||||
ASSERT_TRUE(inMembers.has_value()) << essl;
|
||||
EXPECT_TRUE(Contains(*inMembers, "gl_Position")) << *inMembers;
|
||||
EXPECT_FALSE(Contains(*inMembers, "gl_PointSize"))
|
||||
<< "the `in` block must not pick up the `out` block's members: " << *inMembers;
|
||||
|
||||
const auto outMembers = ExtractPerVertexBlockMembers(essl, false);
|
||||
ASSERT_TRUE(outMembers.has_value()) << essl;
|
||||
EXPECT_TRUE(Contains(*outMembers, "gl_PointSize")) << *outMembers;
|
||||
}
|
||||
|
||||
// A shader that does not redeclare the block must report nothing, so the generator leaves the
|
||||
// driver's built-in declaration alone rather than inventing one.
|
||||
TEST(ExtractPerVertexBlockMembersTest, ReportsNothingWhenTheBlockIsNotRedeclared) {
|
||||
const String essl = R"(#version 320 es
|
||||
layout(quads) in;
|
||||
void main() { gl_Position = gl_in[0].gl_Position; }
|
||||
)";
|
||||
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, true).has_value()) << essl;
|
||||
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, false).has_value()) << essl;
|
||||
}
|
||||
|
||||
// "min" ends in "in" and "layout" ends in "out": the direction keyword has to be a whole token
|
||||
// immediately before the block name, or an unrelated identifier would be read as a redeclaration.
|
||||
TEST(ExtractPerVertexBlockMembersTest, DoesNotMatchAnIdentifierEndingInTheKeyword) {
|
||||
const String essl = R"(#version 320 es
|
||||
struct fin gl_PerVertex { highp vec4 gl_Position; };
|
||||
void main() {}
|
||||
)";
|
||||
EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, true).has_value()) << essl;
|
||||
}
|
||||
|
||||
@@ -598,6 +598,43 @@ namespace MobileGL {
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleReadsLocatedInput(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<spvtools::opt::IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, MakeSpirvMessageConsumer("ModuleReadsLocatedInput"), spirv.data(),
|
||||
spirv.size());
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
// A LOCATION is exactly the property that separates a user-defined varying (or a
|
||||
// per-patch input) from a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn,
|
||||
// gl_PrimitiveID and the tessellation levels carry none, and every one of them is
|
||||
// either forwarded by the pass-through or generated by the tessellator itself.
|
||||
//
|
||||
// Decided on the OpVariable's own Location decoration rather than on any
|
||||
// built-in classification, for the reason DirectVulkan's
|
||||
// ReflectPassthroughTessControlNeed records at length: gl_in is an ARRAY OF
|
||||
// INTERFACE BLOCKS, and a member walk of one reads back as BuiltIn::Position for
|
||||
// every member, so classifying by built-in would accept anything.
|
||||
for (auto& variable : context->module()->types_values()) {
|
||||
if (variable.opcode() != spv::Op::OpVariable || variable.NumInOperands() < 1) {
|
||||
continue;
|
||||
}
|
||||
if (static_cast<spv::StorageClass>(variable.GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
continue;
|
||||
}
|
||||
Bool located = false;
|
||||
context->get_decoration_mgr()->ForEachDecoration(
|
||||
variable.result_id(), static_cast<uint32_t>(spv::Decoration::Location),
|
||||
[&located](const spvtools::opt::Instruction&) { located = true; });
|
||||
if (located) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
|
||||
@@ -427,6 +427,19 @@ namespace MobileGL {
|
||||
// module (see its header for the two operations that make it decline), which is
|
||||
// what the backends report: no mobile driver can build such a module.
|
||||
static Bool ModuleDeclaresFloat64(const Vector<Uint32>& spirv);
|
||||
|
||||
// True when the module declares an Input variable carrying a Location - i.e. a
|
||||
// user-defined varying or a per-patch input, as opposed to a built-in.
|
||||
//
|
||||
// Asked of a TESSELLATION EVALUATION stage that has no control stage, to decide
|
||||
// whether the pass-through control stage GL 4.6 core 11.2.2 describes can stand
|
||||
// in for the missing one. That stage forwards gl_Position and nothing else, so a
|
||||
// located input - which the vertex stage feeds today and which would stop
|
||||
// arriving once a control stage sat in between - means the program has to be
|
||||
// declined rather than fed an undefined varying. Same rule, same reasoning, as
|
||||
// DirectVulkan's ReflectPassthroughTessControlNeed, which asks SPIRV-Reflect the
|
||||
// identical question for the identical decision.
|
||||
static Bool ModuleReadsLocatedInput(const Vector<Uint32>& spirv);
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
|
||||
Reference in New Issue
Block a user