diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index ff866fcb..197cfbce 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -12079,6 +12079,9 @@ namespace MobileGL::MG_Backend::DirectGLES { ? PointSizeExtensionName(g_GLESCapabilities.TessellationPointSizeSupport, /*tessellation=*/true) : nullptr; source = RequestPointSizeExtension(Move(source), passthroughPointSizeExtension); + // Same for gl_ClipDistance/gl_CullDistance mirrored out of a vertex stage that + // writes them: the neighbour could only compile with the extension, so it exists. + source = RequestClipCullDistanceExtension(Move(source)); const GLuint backendShaderId = g_GLESFuncs.glCreateShader(GL_TESS_CONTROL_SHADER); if (backendShaderId == 0) { @@ -12636,6 +12639,14 @@ namespace MobileGL::MG_Backend::DirectGLES { stageAtomicCounterGlBindings.begin(), stageAtomicCounterGlBindings.end()); + // SPIRV-Cross sizes written clip/cull distances with a standalone + // `out float gl_ClipDistance[N];`, which an ES compiler may refuse as a reserved + // name and so lose the whole program. Redeclared as gl_PerVertex members instead. + // BEFORE the gl_PointSize extension probe below (the block can name it) and + // before vertexStageEssl is captured, so a synthesized pass-through control + // stage mirrors the block this produces. + source = RedeclareClipCullDistancesInPerVertexBlock(std::move(source), glShaderType); + // Position in the chain is arbitrary: this is the only header-level rewrite, it // edits #extension directives and never the body, and the replacement is the // same length and stays an #extension line - so it commutes with every pass diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 94eeb4ed..880e265e 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -956,6 +956,116 @@ namespace MobileGL::MG_Backend::DirectGLES { return std::nullopt; } + String RedeclareClipCullDistancesInPerVertexBlock(String glslCode, const GLenum shaderType) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (shaderType != GL_VERTEX_SHADER && shaderType != GL_TESS_EVALUATION_SHADER && + shaderType != GL_GEOMETRY_SHADER) { + return glslCode; + } + if (glslCode.find("gl_ClipDistance[") == String::npos && + glslCode.find("gl_CullDistance[") == String::npos) { + return glslCode; + } + // Whole lines only: the declarations SPIRV-Cross prints are one statement per line, + // and anchoring on the line keeps a use of the array in the body from ever matching. + static const std::regex standaloneRegex( + R"(^\s*out\s+(?:highp\s+)?float\s+(gl_ClipDistance|gl_CullDistance)\s*\[\s*([0-9]+)\s*\]\s*;\s*$)"); + static const std::regex invariantPositionRegex(R"(^\s*invariant\s+gl_Position\s*;\s*$)"); + + Vector lines; + SizeT lineStart = 0; + while (lineStart <= glslCode.size()) { + const SizeT lineEnd = glslCode.find('\n', lineStart); + if (lineEnd == String::npos) { + lines.push_back(glslCode.substr(lineStart)); + break; + } + lines.push_back(glslCode.substr(lineStart, lineEnd - lineStart)); + lineStart = lineEnd + 1; + } + + String clipSize; + String cullSize; + SizeT insertAt = lines.size(); + SizeT invariantLine = lines.size(); + for (SizeT i = 0; i < lines.size(); ++i) { + std::smatch match; + if (std::regex_match(lines[i], match, standaloneRegex)) { + (match[1] == "gl_ClipDistance" ? clipSize : cullSize) = match[2].str(); + if (insertAt == lines.size()) insertAt = i; + } else if (invariantLine == lines.size() && std::regex_match(lines[i], invariantPositionRegex)) { + invariantLine = i; + } + } + if (insertAt == lines.size()) { + return glslCode; + } + + // A WRITE of gl_PointSize is a bare identifier; a read of the previous stage's value + // is a member access on gl_in, which is not this block's business. + const auto isIdentifierChar = [](char c) { + return std::isalnum(static_cast(c)) != 0 || c == '_'; + }; + Bool writesPointSize = false; + static constexpr const char* kPointSize = "gl_PointSize"; + const SizeT pointSizeLength = std::strlen(kPointSize); + for (SizeT pos = glslCode.find(kPointSize); pos != String::npos && !writesPointSize; + pos = glslCode.find(kPointSize, pos + 1)) { + if (pos + pointSizeLength < glslCode.size() && isIdentifierChar(glslCode[pos + pointSizeLength])) { + continue; + } + SizeT before = pos; + while (before > 0 && std::isspace(static_cast(glslCode[before - 1]))) --before; + if (before > 0 && (glslCode[before - 1] == '.' || isIdentifierChar(glslCode[before - 1]))) { + continue; + } + writesPointSize = true; + } + + const Bool invariantPosition = invariantLine != lines.size(); + String block = "out gl_PerVertex\n{\n"; + block += invariantPosition ? " invariant highp vec4 gl_Position;\n" : " highp vec4 gl_Position;\n"; + if (writesPointSize) block += " highp float gl_PointSize;\n"; + if (!clipSize.empty()) block += " highp float gl_ClipDistance[" + clipSize + "];\n"; + if (!cullSize.empty()) block += " highp float gl_CullDistance[" + cullSize + "];\n"; + block += "};"; + + String result; + result.reserve(glslCode.size() + block.size()); + for (SizeT i = 0; i < lines.size(); ++i) { + if (i == insertAt) { + result += block; + } else if (i == invariantLine || std::regex_match(lines[i], standaloneRegex)) { + continue; + } else { + result += lines[i]; + } + if (i + 1 < lines.size()) result += '\n'; + } + return result; + } + + String RequestClipCullDistanceExtension(String glslCode) { + static constexpr const char* kDirective = "#extension GL_EXT_clip_cull_distance : require\n"; + if (glslCode.find("GL_EXT_clip_cull_distance") != String::npos || + (glslCode.find("gl_ClipDistance") == String::npos && glslCode.find("gl_CullDistance") == String::npos)) { + return glslCode; + } + // Right after the #version line, like every other directive this backend adds. + const SizeT versionPos = glslCode.find("#version"); + if (versionPos == String::npos) { + return kDirective + glslCode; + } + const SizeT lineEnd = glslCode.find('\n', versionPos); + if (lineEnd == String::npos) { + return glslCode + "\n" + kDirective; + } + glslCode.insert(lineEnd + 1, kDirective); + return glslCode; + } + String BuildPassthroughTessControlEssl(const Uint esslVersion, const Uint patchVertices, const String& inPerVertexMembers, const String& outPerVertexMembers, diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 3b497103..851a02c9 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -386,6 +386,25 @@ namespace MobileGL::MG_Backend::DirectGLES { // 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); + // Moves the clip/cull distance OUTPUTS of a vertex, tessellation evaluation or geometry + // stage into a redeclared `out gl_PerVertex { ... };` block. SPIRV-Cross sizes the two + // arrays with a standalone `out float gl_ClipDistance[N];` - legal desktop GLSL, but an + // ES compiler may refuse it outright ("reserved built-in name"), and leaving the + // declaration out instead compiles but fails the link on an array-size mismatch against + // the fragment stage. A member of the redeclared block is the spelling ESSL accepts. + // gl_Position is always a member (every pre-rasterization stage has it), gl_PointSize + // only when the stage writes it, so the block never names a built-in the stage would + // otherwise not need. A stage-level `invariant gl_Position;` moves onto the member, + // because the bare re-qualification of a block member is not something ESSL allows. + // Fragment inputs and the control stage (whose block SPIRV-Cross already emits) are + // left alone; a stage without the standalone declarations comes back unchanged. + String RedeclareClipCullDistancesInPerVertexBlock(String glslCode, GLenum shaderType); + // Adds `#extension GL_EXT_clip_cull_distance : require` when `glslCode` names + // gl_ClipDistance or gl_CullDistance and does not request it yet. For the synthesized + // pass-through control stage, whose gl_in/gl_out member lists are mirrored from stages + // that may carry the clip/cull arrays: SPIRV-Cross requests the extension for the stages + // it emits, but nothing does for a stage built by hand. + String RequestClipCullDistanceExtension(String glslCode); // 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. diff --git a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp index a1434e4a..c0a53070 100644 --- a/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp +++ b/MobileGL/MG_Test/Backend/DirectGLES/EsslShaderPassTest.cpp @@ -14,6 +14,11 @@ #include #include +#include +#include +#include + +#include "Init.h" #include @@ -28,10 +33,12 @@ using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_SPLIT_READ_ALIAS_PREFI using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITEONLY_ALIAS_PREFIX; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageArrayUnitPlan; +using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RedeclareClipCullDistancesInPerVertexBlock; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemapImageArrayElementUnits; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::PointSizeExtensionName; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RemoveLayoutBinding; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestPointSizeExtension; +using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestClipCullDistanceExtension; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestExtendedImageFormats; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::RequestViewportArrayExtension; using MobileGL::MG_Backend::DirectGLES::PrgramImpl::SplitReadWriteImageUniforms; @@ -1517,3 +1524,154 @@ void main() {} )"; EXPECT_FALSE(ExtractPerVertexBlockMembers(essl, true).has_value()) << essl; } + +// The shape SPIRV-Cross prints for a vertex stage writing clip and cull distances: standalone, +// sized, `out` redeclarations an ES compiler refuses as reserved names. +TEST(RedeclareClipCullDistancesTest, MovesTheStandaloneArraysIntoTheOutputBlock) { + const String essl = R"(#version 320 es +#extension GL_EXT_clip_cull_distance : require + +out float gl_ClipDistance[4]; +out float gl_CullDistance[1]; + +void main() +{ + gl_Position = vec4(1.0); + gl_ClipDistance[0] = 1.0; + gl_CullDistance[0] = 1.0; +} +)"; + const String out = RedeclareClipCullDistancesInPerVertexBlock(essl, GL_VERTEX_SHADER); + EXPECT_FALSE(Contains(out, "out float gl_ClipDistance")) << out; + EXPECT_FALSE(Contains(out, "out float gl_CullDistance")) << out; + EXPECT_TRUE(Contains(out, "out gl_PerVertex\n{\n highp vec4 gl_Position;\n" + " highp float gl_ClipDistance[4];\n" + " highp float gl_CullDistance[1];\n};")) + << out; + EXPECT_FALSE(Contains(out, "gl_PointSize")) << out; + // The body is untouched. + EXPECT_TRUE(Contains(out, "gl_ClipDistance[0] = 1.0;")) << out; + EXPECT_EQ(CountOf(out, "gl_PerVertex"), 1u) << out; + // What a synthesized pass-through control stage would mirror from this stage. + const auto members = ExtractPerVertexBlockMembers(out, /*input=*/false); + ASSERT_TRUE(members.has_value()) << out; + EXPECT_TRUE(Contains(*members, "highp float gl_ClipDistance[4];")) << *members; +} + +TEST(RedeclareClipCullDistancesTest, KeepsAWrittenPointSizeAndFoldsInvariance) { + const String essl = R"(#version 320 es +#extension GL_EXT_clip_cull_distance : require +layout(points) in; +layout(points, max_vertices = 1) out; + +out float gl_ClipDistance[2]; + +invariant gl_Position; + +void main() +{ + gl_Position = gl_in[0].gl_Position; + gl_PointSize = gl_in[0].gl_PointSize; + gl_ClipDistance[1] = 0.5; + EmitVertex(); +} +)"; + const String out = RedeclareClipCullDistancesInPerVertexBlock(essl, GL_GEOMETRY_SHADER); + EXPECT_TRUE(Contains(out, " invariant highp vec4 gl_Position;\n highp float gl_PointSize;\n" + " highp float gl_ClipDistance[2];\n};")) + << out; + EXPECT_FALSE(Contains(out, "invariant gl_Position;")) << out; + EXPECT_FALSE(Contains(out, "out float gl_ClipDistance")) << out; +} + +// Only a READ of the previous stage's point size: not this stage's output, so not a member. +TEST(RedeclareClipCullDistancesTest, AnInputPointSizeReadIsNotAMember) { + const String essl = R"(#version 320 es +layout(triangles) in; +out float gl_ClipDistance[1]; +void main() +{ + gl_Position = gl_in[0].gl_Position * gl_in[0].gl_PointSize; + gl_ClipDistance[0] = 1.0; +} +)"; + const String out = RedeclareClipCullDistancesInPerVertexBlock(essl, GL_TESS_EVALUATION_SHADER); + EXPECT_TRUE(Contains(out, "out gl_PerVertex")) << out; + EXPECT_FALSE(Contains(out, "highp float gl_PointSize;")) << out; +} + +TEST(RedeclareClipCullDistancesTest, LeavesFragmentInputsControlStagesAndPlainShadersAlone) { + const String fragment = R"(#version 320 es +in float gl_ClipDistance[4]; +layout(location = 0) out highp vec4 c; +void main() { c = vec4(gl_ClipDistance[0]); } +)"; + EXPECT_EQ(RedeclareClipCullDistancesInPerVertexBlock(fragment, GL_FRAGMENT_SHADER), fragment); + const String control = R"(#version 320 es +layout(vertices = 3) out; +out gl_PerVertex +{ + vec4 gl_Position; + float gl_ClipDistance[1]; +} gl_out[3]; +void main() { gl_out[gl_InvocationID].gl_ClipDistance[0] = 1.0; } +)"; + EXPECT_EQ(RedeclareClipCullDistancesInPerVertexBlock(control, GL_TESS_CONTROL_SHADER), control); + const String plain = "#version 320 es\nvoid main()\n{\n gl_Position = vec4(0.0);\n}\n"; + EXPECT_EQ(RedeclareClipCullDistancesInPerVertexBlock(plain, GL_VERTEX_SHADER), plain); +} + +TEST(RequestClipCullDistanceExtensionTest, AddsTheDirectiveOnlyWhenTheArraysAreNamed) { + const String mirrored = "#version 320 es\nin gl_PerVertex { highp vec4 gl_Position; highp float " + "gl_ClipDistance[2]; } gl_in[gl_MaxPatchVertices];\nvoid main() {}\n"; + const String out = RequestClipCullDistanceExtension(mirrored); + EXPECT_EQ(out.find("#version 320 es\n#extension GL_EXT_clip_cull_distance : require\n"), 0u) << out; + EXPECT_EQ(RequestClipCullDistanceExtension(out), out); + const String plain = "#version 320 es\nvoid main() {}\n"; + EXPECT_EQ(RequestClipCullDistanceExtension(plain), plain); +} + +// End to end against what SPIRV-Cross really prints, so the fixtures above cannot drift from it. +TEST(RedeclareClipCullDistancesTest, RewritesRealSpirvCrossOutput) { + using namespace MG_Util::ShaderTranspiler; + MobileGL::Initialize(); + const char* vertexSource = R"(#version 330 core +out float gl_ClipDistance[3]; +void main() { + gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0); + for (int i = 0; i < 3; ++i) gl_ClipDistance[i] = float(i) - 1.0; +} +)"; + const char* fragmentSource = R"(#version 330 core +out vec4 color; +void main() { color = vec4(1.0); } +)"; + Vector> shaders; + const Vector> stages{{GL_VERTEX_SHADER, vertexSource}, + {GL_FRAGMENT_SHADER, fragmentSource}}; + for (const auto& [stage, text] : stages) { + ShaderAttrib attrib{.shaderType = stage, .sourceStr = text}; + auto compiled = ShaderCompiler::CompileShader(attrib); + ASSERT_TRUE(compiled) << compiled.error().log; + shaders.push_back(compiled.value()); + } + ProgramAttrib programAttrib{.shaders = shaders}; + auto program = ShaderCompiler::LinkProgram(programAttrib); + ASSERT_TRUE(program) << program.error().log; + ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER}, + .program = *program.value()}; + auto modules = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + ASSERT_TRUE(modules) << modules.error().log; + ASSERT_EQ(modules.value().size(), 2u); + + SpvcSession session(modules.value()[0], SessionUsageBit::Transpile); + auto essl = ShaderCompiler::DecompileShader(session); + ASSERT_TRUE(essl) << essl.error().log; + // The shape the pass exists for really is what SPIRV-Cross prints. + ASSERT_TRUE(Contains(essl.value(), "out float gl_ClipDistance[3];")) << essl.value(); + + const String out = RedeclareClipCullDistancesInPerVertexBlock(essl.value(), GL_VERTEX_SHADER); + EXPECT_FALSE(Contains(out, "out float gl_ClipDistance")) << out; + EXPECT_TRUE(Contains(out, " highp float gl_ClipDistance[3];\n};")) << out; + EXPECT_TRUE(Contains(out, "GL_EXT_clip_cull_distance")) << out; +}