diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index df9c4137..513aeb81 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -12047,6 +12047,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // stage never reaches the program. spvcSession.RelaxReadWriteExclusiveStorageBuffers(); + // gl_ShadingRateEXT / gl_PrimitiveShadingRateEXT: SPIRV-Cross spells them only for + // Vulkan GLSL, so they go through under a sentinel name and are restored below. Only + // a program whose stages use them gets here with anything to do, and only a driver + // with GL_EXT_fragment_shading_rate gets such a program past the frontend. + const Bool restoreShadingRateBuiltins = spvcSession.PrepareShadingRateBuiltinsForEssl(); + const char* result = nullptr; spvcSession.Compile(&result); @@ -12060,6 +12066,9 @@ namespace MobileGL::MG_Backend::DirectGLES { } outSource = result; + if (restoreShadingRateBuiltins) { + MG_Util::ShaderTranspiler::SpvcSession::FinishShadingRateBuiltinsForEssl(outSource); + } return true; } diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index 284ba0f4..c2768ce3 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -21,6 +21,7 @@ add_executable( DemotePointSizeTest.cpp FragCoordYFlipTest.cpp RelaxedOpaqueStructTest.cpp + ShadingRateEsslTest.cpp ) target_include_directories(SpirvPassTest PRIVATE diff --git a/MobileGL/MG_Test/ShaderTranspiler/ShadingRateEsslTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/ShadingRateEsslTest.cpp new file mode 100644 index 00000000..a2489502 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/ShadingRateEsslTest.cpp @@ -0,0 +1,146 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/ShadingRateEsslTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// GL_EXT_fragment_shading_rate's built-ins on the ESSL leg. glslang lowers gl_ShadingRateEXT and +// gl_PrimitiveShadingRateEXT to the ShadingRateKHR / PrimitiveShadingRateKHR built-ins, which +// SPIRV-Cross spells only for Vulkan GLSL - for ESSL it refused the whole stage. SpvcSession's +// Prepare/FinishShadingRateBuiltinsForEssl pair carries them through under a sentinel name and +// puts the ESSL built-in back; these cases pin the emitted text. + +#include + +#include +#include + +#include "Includes.h" +#include "Init.h" +#include +#include +#include +#include + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; +using MobileGL::MG_Util::ShaderTranspiler::SpvcSession; + +namespace { + Vector> CompileProgramToSpirv(const Vector>& stages) { + using namespace MG_Util::ShaderTranspiler; + Vector> shaders; + Vector types; + // The frontend's own source pass first: it is what retargets the ES sources the suite + // submits to the desktop profile glslang compiles them under. + Vector sources; + sources.reserve(stages.size()); + for (const auto& [stage, source] : stages) { + sources.emplace_back(source); + PreprocessShaderSource(stage == GL_VERTEX_SHADER ? ShaderStage::Vertex : ShaderStage::Fragment, + sources.back()); + ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = sources.back()}; + auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib); + EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log); + if (!shaderResult) return {}; + shaders.push_back(shaderResult.value()); + types.push_back(stage); + } + ProgramAttrib programAttrib{.shaders = shaders}; + auto programResult = ShaderCompiler::LinkProgram(programAttrib); + EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log); + if (!programResult) return {}; + ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *programResult.value()}; + auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log); + if (!binaryResult) return {}; + Vector> modules = Move(binaryResult.value()); + for (auto& module : modules) { + EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true)); + } + return modules; + } + + String Transpile(const Vector& spirv) { + SpvcSession session(spirv, SessionUsageBit::Transpile); + auto essl = ShaderCompiler::DecompileShader(session); + EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log); + return essl ? essl.value() : String{}; + } + + // The KHR-GL4x.fragment_shading_rate.complex.* pair, primitive variant. + constexpr const char* kVertex = "#version 310 es\n" + "#extension GL_EXT_fragment_shading_rate : enable\n" + "precision highp float;\n" + "precision highp int;\n" + "layout(location = 0) in vec4 position;\n" + "uniform int primShadingRate;\n" + "void main() {\n" + " gl_Position = position;\n" + " gl_PrimitiveShadingRateEXT = primShadingRate;\n" + "}\n"; + constexpr const char* kFragment = "#version 310 es\n" + "#extension GL_EXT_fragment_shading_rate : enable\n" + "precision highp float;\n" + "precision highp int;\n" + "layout(location = 0) out ivec4 color0;\n" + "uniform int shadingRate;\n" + "void main() {\n" + " color0.x = gl_ShadingRateEXT;\n" + " color0.w = gl_ShadingRateEXT != shadingRate ? 1 : 0;\n" + "}\n"; +} // namespace + +class ShadingRateEsslTest: public ::testing::Test { +protected: + void SetUp() override { MobileGL::Initialize(); } +}; + +TEST_F(ShadingRateEsslTest, BothBuiltInsReachTheEsslUnderTheirExtensionNames) { + const auto modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertex}, {GL_FRAGMENT_SHADER, kFragment}}); + ASSERT_EQ(modules.size(), 2u); + + for (SizeT i = 0; i < modules.size(); ++i) { + const String essl = Transpile(modules[i]); + ASSERT_FALSE(essl.empty()) << "stage " << i << " was refused"; + const char* builtin = i == 0 ? "gl_PrimitiveShadingRateEXT" : "gl_ShadingRateEXT"; + EXPECT_NE(essl.find(builtin), String::npos) << essl; + EXPECT_NE(essl.find("#extension GL_EXT_fragment_shading_rate : require"), String::npos) << essl; + EXPECT_EQ(essl.find("mgl_sentinel"), String::npos) << "a sentinel survived:\n" << essl; + // The built-in is the driver's; redeclaring it as an interface variable is illegal ESSL. + EXPECT_EQ(essl.find(String("int ") + builtin + ";"), String::npos) << essl; + } +} + +TEST_F(ShadingRateEsslTest, AStageWithoutTheBuiltInsIsLeftAlone) { + constexpr const char* kPlainFragment = "#version 310 es\n" + "precision highp float;\n" + "layout(location = 0) out vec4 color0;\n" + "void main() { color0 = vec4(1.0); }\n"; + const auto modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kVertex}, {GL_FRAGMENT_SHADER, kPlainFragment}}); + ASSERT_EQ(modules.size(), 2u); + const String essl = Transpile(modules[1]); + ASSERT_FALSE(essl.empty()); + EXPECT_EQ(essl.find("GL_EXT_fragment_shading_rate"), String::npos) << essl; +} + +TEST(ShadingRateEsslFixupTest, OnlyTheDeclarationIsDroppedAndOnlyWholeIdentifiersAreRenamed) { + String essl = "#version 320 es\n" + "flat in highp int mgl_sentinel_ShadingRateEXT;\n" + "layout(location = 3) flat in highp int mgl_sentinel_ShadingRateEXT;\n" + "int mgl_sentinel_ShadingRateEXTx = 1;\n" + "void main() {\n" + " int a = mgl_sentinel_ShadingRateEXT;\n" + " a += (mgl_sentinel_ShadingRateEXT & 3);\n" + "}\n"; + SpvcSession::FinishShadingRateBuiltinsForEssl(essl); + EXPECT_EQ(essl, "#version 320 es\n" + "int mgl_sentinel_ShadingRateEXTx = 1;\n" + "void main() {\n" + " int a = gl_ShadingRateEXT;\n" + " a += (gl_ShadingRateEXT & 3);\n" + "}\n"); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index a6e94efd..982cf7d1 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -2315,6 +2315,8 @@ namespace MobileGL { spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE); session.SetOptions(options); + // Same ESSL spelling of the shading-rate built-ins DirectGLES emits. + const Bool restoreShadingRateBuiltins = session.PrepareShadingRateBuiltinsForEssl(); const char* result = nullptr; session.Compile(&result); @@ -2328,6 +2330,9 @@ namespace MobileGL { } std::string glsl = result; + if (restoreShadingRateBuiltins) { + SpvcSession::FinishShadingRateBuiltinsForEssl(glsl); + } return glsl; } diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp index 0a2d6ecd..8fa2d4c3 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp @@ -791,6 +791,127 @@ namespace MobileGL { return true; } + namespace { + struct ShadingRateBuiltinSpelling { + SpvBuiltIn builtin; + const char* sentinel; + const char* essl; + }; + // The sentinels only have to survive SPIRV-Cross's renaming untouched (no "gl_" + // prefix, no double underscore) and be names no application spells. + constexpr ShadingRateBuiltinSpelling kShadingRateBuiltins[] = { + {SpvBuiltInShadingRateKHR, "mgl_sentinel_ShadingRateEXT", "gl_ShadingRateEXT"}, + {SpvBuiltInPrimitiveShadingRateKHR, "mgl_sentinel_PrimitiveShadingRateEXT", + "gl_PrimitiveShadingRateEXT"}, + }; + + Bool IsIdentifierChar(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; + } + + // Whether `line` declares `name` as a plain interface variable - the one statement + // SPIRV-Cross prints for an undecorated in/out: optional layout(...), interpolation + // and precision qualifiers, `in`/`out`, a type, the name and the semicolon. No '=', + // no call, nothing after the name: every USE of the variable is an expression. + Bool IsInterfaceDeclarationOf(const String& line, const String& name) { + const SizeT end = line.find_last_not_of(" \t\r"); + if (end == String::npos || end == 0 || line[end] != ';') return false; + SizeT nameEnd = line.find_last_not_of(" \t", end - 1); + if (nameEnd == String::npos || nameEnd + 1 < name.size()) return false; + const SizeT nameBegin = nameEnd + 1 - name.size(); + if (line.compare(nameBegin, name.size(), name) != 0) return false; + if (nameBegin > 0 && IsIdentifierChar(line[nameBegin - 1])) return false; + String head = line.substr(0, nameBegin); + // Drop a leading layout(...) block, then require the storage keyword as a word. + const SizeT layout = head.find("layout"); + if (layout != String::npos) { + const SizeT close = head.find(')', layout); + if (close == String::npos) return false; + head.erase(layout, close + 1 - layout); + } + if (head.find('(') != String::npos || head.find('=') != String::npos) return false; + for (const char* keyword : {"in", "out"}) { + const String word(keyword); + for (SizeT at = head.find(word); at != String::npos; at = head.find(word, at + 1)) { + const Bool startOk = at == 0 || !IsIdentifierChar(head[at - 1]); + const Bool endOk = at + word.size() >= head.size() || !IsIdentifierChar(head[at + word.size()]); + if (startOk && endOk) return true; + } + } + return false; + } + } // namespace + + Bool SpvcSession::PrepareShadingRateBuiltinsForEssl() { + if (!(usage & SessionUsageBit::Transpile) || !IsTranspileReady()) return false; + + Bool renamed = false; + for (const spvc_builtin_resource_type type : + {SPVC_BUILTIN_RESOURCE_TYPE_STAGE_INPUT, SPVC_BUILTIN_RESOURCE_TYPE_STAGE_OUTPUT}) { + const spvc_reflected_builtin_resource* list = nullptr; + size_t count = 0; + if (spvc_resources_get_builtin_resource_list_for_type(resources, type, &list, &count) != + SPVC_SUCCESS) { + continue; + } + for (size_t i = 0; i < count; ++i) { + const SpvId variable = list[i].resource.id; + // A member of a built-in block is reported here too; only a standalone + // variable carries the decoration itself, and that is how both of these + // are declared in every stage that has them. + if (!spvc_compiler_has_decoration(compiler, variable, SpvDecorationBuiltIn)) continue; + for (const auto& spelling : kShadingRateBuiltins) { + if (list[i].builtin != spelling.builtin) continue; + spvc_compiler_unset_decoration(compiler, variable, SpvDecorationBuiltIn); + spvc_compiler_set_name(compiler, variable, spelling.sentinel); + renamed = true; + } + } + } + if (renamed) { + spvc_compiler_require_extension(compiler, "GL_EXT_fragment_shading_rate"); + } + return renamed; + } + + void SpvcSession::FinishShadingRateBuiltinsForEssl(String& essl) { + for (const auto& spelling : kShadingRateBuiltins) { + const String sentinel = spelling.sentinel; + if (essl.find(sentinel) == String::npos) continue; + + String rewritten; + rewritten.reserve(essl.size()); + SizeT lineBegin = 0; + while (lineBegin < essl.size()) { + SizeT lineEnd = essl.find('\n', lineBegin); + const SizeT next = lineEnd == String::npos ? essl.size() : lineEnd + 1; + const String line = essl.substr(lineBegin, (lineEnd == String::npos ? essl.size() : lineEnd) - + lineBegin); + if (!IsInterfaceDeclarationOf(line, sentinel)) { + rewritten.append(essl, lineBegin, next - lineBegin); + } + lineBegin = next; + } + + // Every remaining occurrence is a use; rename it, as a whole identifier only. + String result; + result.reserve(rewritten.size()); + SizeT cursor = 0; + for (SizeT at = rewritten.find(sentinel); at != String::npos; + at = rewritten.find(sentinel, at + sentinel.size())) { + const Bool startOk = at == 0 || !IsIdentifierChar(rewritten[at - 1]); + const SizeT after = at + sentinel.size(); + const Bool endOk = after >= rewritten.size() || !IsIdentifierChar(rewritten[after]); + if (!startOk || !endOk) continue; + result.append(rewritten, cursor, at - cursor); + result.append(spelling.essl); + cursor = after; + } + result.append(rewritten, cursor, String::npos); + essl = Move(result); + } + } + spvc_result SpvcSession::Compile(const char** result) { if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT; SPVC_CHK_INIT diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h index 3f460548..46ac8088 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.h @@ -174,6 +174,20 @@ namespace MobileGL { // A block carrying only ONE of the two is left exactly as it is: those really do // constrain the accesses the shader makes, and the driver is entitled to know. spvc_result RelaxReadWriteExclusiveStorageBuffers(); + // GL_EXT_fragment_shading_rate in ESSL, in two halves around Compile(). + // + // GLSL ES has the two built-ins - gl_ShadingRateEXT (fragment input) and + // gl_PrimitiveShadingRateEXT (vertex / geometry output) - under + // GL_EXT_fragment_shading_rate, but SPIRV-Cross only spells their SPIR-V + // counterparts (ShadingRateKHR / PrimitiveShadingRateKHR) for Vulkan GLSL and + // refuses the stage otherwise. So the first half strips the BuiltIn decoration + // from those variables, names them with a sentinel and requires the extension; + // SPIRV-Cross then emits them as an ordinary declaration plus ordinary uses, and + // FinishShadingRateBuiltinsForEssl deletes that declaration and puts the built-in + // name back at every use. Returns whether any variable was renamed - when it + // returns false the second half has nothing to do. + Bool PrepareShadingRateBuiltinsForEssl(); + static void FinishShadingRateBuiltinsForEssl(String& essl); // ---- GL_ARB_gl_spirv: an APPLICATION-supplied module, not one MobileGL emitted ---- // Select which OpEntryPoint of `model` this session compiles. A module may carry // several of the same execution model, and glSpecializeShader names the one the