mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (Espryt): redeclare clip/cull distance outputs inside gl_PerVertex for ESSL
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<String> 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<unsigned char>(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<unsigned char>(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,
|
||||
|
||||
@@ -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<String> 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.
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <MG_Backend/DirectGLES/Utils.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include "Init.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
@@ -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<SharedPtr<glslang::TShader>> shaders;
|
||||
const Vector<Pair<GLenum, const char*>> 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user