[Fix, Test] (DirectGLES, ShaderTranspiler): synthesize the pass-through tessellation control stage ES requires

This commit is contained in:
2026-08-21 05:23:57 -04:00
parent d9def5c1bb
commit 50da7de737
8 changed files with 443 additions and 1 deletions
+10 -1
View File
@@ -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();
+157
View File
@@ -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.
+23
View File
@@ -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;
+76
View File
@@ -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 == '_';
+44
View File
@@ -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