diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index a09886f9..c3cc734c 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -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(MG_State::pGLContext->GetPatchVertices()))) { twin->SyncToBackend(currentProgram); } g_currentDrawFrontendProgram = currentProgram.get(); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 9220ac87..cf8a4134 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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>& 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(patchVertices); + + if (tessEvalShaderIndex < 0 || + static_cast(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(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 log(static_cast(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& 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. diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index a0b54dd2..0cea95e9 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -1222,6 +1222,15 @@ namespace MobileGL::MG_Backend::DirectGLES { // counter sync at one empty-vector test. const Vector& 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& GetUniformBlockBackendIndices() const { return m_uniformBlockBackendIndices; } @@ -1270,6 +1279,16 @@ namespace MobileGL::MG_Backend::DirectGLES { private: void CacheResourceLocations(const SharedPtr& 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>& 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 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; diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index d805ef9e..aaffdcb2 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -746,6 +746,82 @@ namespace MobileGL::MG_Backend::DirectGLES { return result; } + std::optional 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(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(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(c)) || c == '_'; diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index e637d6c3..6f3a2d18 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -280,6 +280,50 @@ namespace MobileGL::MG_Backend::DirectGLES { // it reads need no entry in BuildEsslTranslationKey. String RemapImageArrayElementUnits(const String& glslCode, const Vector& plans, Int stageImageUniformBudget, Vector* 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 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_"; diff --git a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp index c27f2f9e..7e4acfb1 100644 --- a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp +++ b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp @@ -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; +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 1e5c5b70..df9301ee 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -598,6 +598,43 @@ namespace MobileGL { return false; } + Bool ShaderCompiler::ModuleReadsLocatedInput(const Vector& spirv) { + if (spirv.empty()) { + return false; + } + std::unique_ptr 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(variable.GetSingleWordInOperand(0)) != + spv::StorageClass::Input) { + continue; + } + Bool located = false; + context->get_decoration_mgr()->ForEachDecoration( + variable.result_id(), static_cast(spv::Decoration::Location), + [&located](const spvtools::opt::Instruction&) { located = true; }); + if (located) return true; + } + return false; + } + bool ShaderCompiler::DemoteFloat64ToFloat32(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index bf92aafd..70664175 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -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& 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& spirv); }; } // namespace ShaderTranspiler } // namespace MG_Util