[Feat] (ShaderTranspiler): carry shading-rate built-ins through the ESSL leg

This commit is contained in:
2026-10-12 08:03:28 -04:00
parent 7e544c2b78
commit 6d27499f3c
6 changed files with 296 additions and 0 deletions
@@ -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;
}
@@ -21,6 +21,7 @@ add_executable(
DemotePointSizeTest.cpp
FragCoordYFlipTest.cpp
RelaxedOpaqueStructTest.cpp
ShadingRateEsslTest.cpp
)
target_include_directories(SpirvPassTest PRIVATE
@@ -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 <gtest/gtest.h>
#include <string>
#include <vector>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
#include <MG_Util/ShaderTranspiler/Types.h>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
namespace {
Vector<Vector<Uint32>> CompileProgramToSpirv(const Vector<Pair<GLenum, const char*>>& stages) {
using namespace MG_Util::ShaderTranspiler;
Vector<SharedPtr<glslang::TShader>> shaders;
Vector<GLenum> 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<String> 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<Vector<Uint32>> modules = Move(binaryResult.value());
for (auto& module : modules) {
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
}
return modules;
}
String Transpile(const Vector<Uint32>& 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");
}
@@ -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;
}
@@ -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
@@ -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