mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[Feat] (ShaderTranspiler): carry shading-rate built-ins through the ESSL leg
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user