[Feat] (ShaderTranspiler, DirectGLES): support noperspective conformantly instead of stripping it - let the qualifier reach glslang as the core SPIR-V NoPerspective decoration (native on DirectVulkan; SPIRV-Cross emits ESSL noperspective + GL_NV_shader_noperspective_interpolation on DirectGLES), and for GLES devices lacking that extension add StripNoPerspectivePass to drop the decoration and fall back to smooth; the old naked substring erase discarded the interpolation shader packs need and mangled identifiers containing the word

This commit is contained in:
2026-07-20 22:59:43 -04:00
parent b6a7807a3a
commit bce9c48c8e
10 changed files with 323 additions and 9 deletions
+1
View File
@@ -192,6 +192,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
@@ -2790,6 +2790,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &uboPrecisionSpirv;
}
// noperspective is core desktop GLSL and reaches here as the SPIR-V NoPerspective
// decoration. SPIRV-Cross renders it as ESSL `noperspective` + `#extension
// GL_NV_shader_noperspective_interpolation : require`; on a driver without that
// extension the require fails, so strip the decoration first and let the varying
// fall back to smooth interpolation. Devices that have the extension keep the
// decoration and get true screen-linear interpolation.
Vector<unsigned int> noperspectiveSpirv;
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
MG_Util::ShaderTranspiler::ShaderCompiler::StripNoPerspectiveForEssl(
*effectiveSpirv, noperspectiveSpirv) &&
!noperspectiveSpirv.empty()) {
effectiveSpirv = &noperspectiveSpirv;
}
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
@@ -1089,6 +1089,174 @@ TEST_F(ProgramUtilTest, CompileFragmentShaderWithDiscard) {
}
}
// noperspective is core desktop GLSL (1.30+) and maps to the SPIR-V NoPerspective decoration. It must
// reach glslang (not be stripped as text) so the SPIR-V carries the decoration; SPIRV-Cross then emits
// ESSL `noperspective` + the GL_NV_shader_noperspective_interpolation extension. Shader packs
// (Iris/Complementary) depend on it, and KHR-GL33.glsl_noperspective fails if the result matches
// smooth. This is the DirectGLES path with the NV extension available (SPIRV-Cross's default).
TEST_F(ProgramUtilTest, NoperspectiveInterpolationSurvivesToEssl) {
using namespace MG_Util::ShaderTranspiler;
String fs = R"(#version 330 core
noperspective in vec4 vColor;
out vec4 fragColor;
void main() { fragColor = vColor; }
)";
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) FAIL() << "compile errc: " << res.error().errc << "\nlog: " << res.error().log;
ProgramAttrib programAttrib{.shaders = {res.value()}};
auto program_res = ShaderCompiler::LinkProgram(programAttrib);
if (!program_res) FAIL() << "link errc: " << program_res.error().errc << "\nlog: " << program_res.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program_res.value()};
auto bin_res = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
if (!bin_res) FAIL() << "spirv errc: " << bin_res.error().errc << "\nlog: " << bin_res.error().log;
ASSERT_EQ(bin_res.value().size(), 1u);
SpvcSession session(bin_res.value()[0], SessionUsageBit::Transpile);
auto essl = ShaderCompiler::DecompileShader(session);
if (!essl) FAIL() << "decompile errc: " << essl.error().errc << "\nlog: " << essl.error().log;
EXPECT_NE(essl.value().find("noperspective"), String::npos)
<< "noperspective was lost before it reached SPIR-V:\n" << essl.value();
EXPECT_NE(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos)
<< "SPIRV-Cross must require the NV extension for ES noperspective:\n" << essl.value();
}
// The old handling was a naked substring erase of "noperspective", so any identifier that merely
// contained those characters (a uniform named noperspectiveBlend, say) got mangled. Removing the
// strip fixes it - glslang, which is identifier-aware, is the only thing that should see the keyword.
TEST_F(ProgramUtilTest, PreprocessDoesNotCorruptIdentifiersContainingNoperspective) {
using namespace MG_Util::ShaderTranspiler;
String source = R"(#version 330 core
uniform float noperspectiveBlend;
out vec4 fragColor;
void main() { fragColor = vec4(noperspectiveBlend); }
)";
PreprocessShaderSource(ShaderStage::Fragment, source);
EXPECT_NE(source.find("noperspectiveBlend"), String::npos)
<< "identifier was corrupted by substring stripping:\n" << source;
}
// The DirectGLES fallback for devices without GL_NV_shader_noperspective_interpolation: stripping the
// NoPerspective decoration makes SPIRV-Cross emit a plain smooth varying with no `#extension … :
// require`, so the shader still compiles (rendering as smooth) instead of being rejected by the driver.
TEST_F(ProgramUtilTest, StripNoPerspectiveFallbackProducesPlainEssl) {
using namespace MG_Util::ShaderTranspiler;
String fs = R"(#version 330 core
noperspective in vec4 vColor;
out vec4 fragColor;
void main() { fragColor = vColor; }
)";
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) FAIL() << "compile errc: " << res.error().errc << "\nlog: " << res.error().log;
ProgramAttrib programAttrib{.shaders = {res.value()}};
auto program_res = ShaderCompiler::LinkProgram(programAttrib);
if (!program_res) FAIL() << "link errc: " << program_res.error().errc << "\nlog: " << program_res.error().log;
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *program_res.value()};
auto bin_res = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
if (!bin_res) FAIL() << "spirv errc: " << bin_res.error().errc << "\nlog: " << bin_res.error().log;
ASSERT_EQ(bin_res.value().size(), 1u);
// Precondition: with the decoration present the default decompile requires the NV extension.
{
SpvcSession session(bin_res.value()[0], SessionUsageBit::Transpile);
auto essl = ShaderCompiler::DecompileShader(session);
if (!essl) FAIL() << "decompile errc: " << essl.error().errc;
ASSERT_NE(essl.value().find("noperspective"), String::npos) << essl.value();
}
// The fallback strips the decoration -> plain smooth ESSL, no extension require.
Vector<Uint32> stripped;
ASSERT_TRUE(ShaderCompiler::StripNoPerspectiveForEssl(bin_res.value()[0], stripped));
ASSERT_FALSE(stripped.empty());
SpvcSession session(stripped, SessionUsageBit::Transpile);
auto essl = ShaderCompiler::DecompileShader(session);
if (!essl) FAIL() << "decompile errc: " << essl.error().errc << "\nlog: " << essl.error().log;
EXPECT_EQ(essl.value().find("noperspective"), String::npos)
<< "the decoration should be gone:\n" << essl.value();
EXPECT_EQ(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos)
<< "no extension require without the decoration:\n" << essl.value();
}
// Directly exercises BOTH decoration forms StripNoPerspectivePass handles: a plain-variable
// OpDecorate NoPerspective (in-operand 1) and an interface-block-member OpMemberDecorate NoPerspective
// (in-operand 2). The ESSL round-trip tests above use only a scalar input, so they never reach the
// member-decorate branch, which a block varying like `in Block { noperspective vec4 c; }` (common in
// shader packs) produces. Unrelated decorations (Flat, Location) must survive untouched.
TEST_F(ProgramUtilTest, StripNoPerspectivePassRemovesBothDecorateForms) {
using namespace MG_Util::ShaderTranspiler;
const String spirvText = R"(
OpCapability Shader
OpMemoryModel Logical GLSL450
OpEntryPoint Fragment %main "main" %plainVar %blockVar %flatVar
OpExecutionMode %main OriginUpperLeft
OpName %main "main"
OpDecorate %plainVar Location 0
OpDecorate %plainVar NoPerspective
OpMemberDecorate %Block 0 NoPerspective
OpDecorate %blockVar Location 1
OpDecorate %flatVar Location 2
OpDecorate %flatVar Flat
%void = OpTypeVoid
%mainFn = OpTypeFunction %void
%float = OpTypeFloat 32
%v4float = OpTypeVector %float 4
%int = OpTypeInt 32 1
%inV4Ptr = OpTypePointer Input %v4float
%plainVar = OpVariable %inV4Ptr Input
%Block = OpTypeStruct %v4float
%inBlockPtr = OpTypePointer Input %Block
%blockVar = OpVariable %inBlockPtr Input
%inIntPtr = OpTypePointer Input %int
%flatVar = OpVariable %inIntPtr Input
%main = OpFunction %void None %mainFn
%mainBody = OpLabel
OpReturn
OpFunctionEnd
)";
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
Vector<uint32_t> inputBinary;
ASSERT_TRUE(tools.Assemble(spirvText, &inputBinary));
const auto countNoPerspective = [](const String& text) {
SizeT count = 0, offset = 0;
while ((offset = text.find("NoPerspective", offset)) != String::npos) {
++count;
offset += std::strlen("NoPerspective");
}
return count;
};
String inputText;
ASSERT_TRUE(tools.Disassemble(inputBinary, &inputText));
ASSERT_EQ(countNoPerspective(inputText), 2u)
<< "fixture must carry both a plain and a member NoPerspective:\n" << inputText;
Vector<uint32_t> outputBinary;
ASSERT_TRUE(ShaderCompiler::StripNoPerspectiveForEssl(inputBinary, outputBinary));
ASSERT_FALSE(outputBinary.empty());
String outputText;
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
EXPECT_EQ(countNoPerspective(outputText), 0u)
<< "both NoPerspective decorations (OpDecorate and OpMemberDecorate) must be stripped:\n" << outputText;
EXPECT_NE(outputText.find("Flat"), String::npos)
<< "the unrelated Flat decoration must survive:\n" << outputText;
EXPECT_NE(outputText.find("Location"), String::npos)
<< "Location decorations must survive:\n" << outputText;
}
const char* vs_location = R"(#version 460
in vec4 Position;
@@ -811,6 +811,9 @@ namespace MobileGL::MG_Util::BackendLoader {
if (std::strcmp(extension, "GL_EXT_blend_func_extended") == 0) {
caps.SupportsDualSourceBlend = true;
}
if (std::strcmp(extension, "GL_NV_shader_noperspective_interpolation") == 0) {
caps.SupportsNoperspectiveInterpolation = true;
}
}
}
@@ -1050,6 +1050,11 @@ namespace MobileGL {
// factors and layout(index = 1) fragment outputs. GLES core has no dual-source blending,
// so without this a draw using a SRC1 factor cannot proceed.
Bool SupportsDualSourceBlend = false;
// GL_NV_shader_noperspective_interpolation is present: the driver accepts the
// `noperspective` interpolation qualifier in ESSL. GLES core has none, so without this
// SPIRV-Cross's `#extension ... : require` would fail to compile and MobileGL falls back
// to stripping the NoPerspective decoration (smooth interpolation) via StripNoPerspectivePass.
Bool SupportsNoperspectiveInterpolation = false;
// GL_RENDERER contains "ANGLE".
Bool IsAngleRenderer = false;
// GL_RENDERER contains both "ANGLE" and "llvmpipe".
@@ -20,6 +20,7 @@
#include "SpirvPasses/LowerDrawParametersPass.h"
#include "SpirvPasses/RebaseInstanceIndexPass.h"
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
#include "SpirvPasses/StripNoPerspectivePass.h"
#include "spirv-tools/libspirv.h"
#include "spirv-tools/optimizer.hpp"
@@ -334,6 +335,18 @@ namespace MobileGL {
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(StripNoPerspectivePass::CreateStripNoPerspectivePass());
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
@@ -33,6 +33,11 @@ namespace MobileGL {
// Only for the DirectGLES transpile path.
static bool StripUboMemberRelaxedPrecisionForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Removes NoPerspective decorations so SPIRV-Cross emits plain (smooth) ESSL varyings.
// DirectGLES fallback only, for devices lacking GL_NV_shader_noperspective_interpolation
// (SPIRV-Cross would otherwise require that extension and the driver would reject it).
static bool StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
@@ -1251,15 +1251,13 @@ namespace MobileGL {
NormalizeLineDirectives(source);
// remove "noperspective"
const char* str_np = "noperspective";
const SizeT len_np = strlen(str_np);
SizeT noperspectivePos = source.find(str_np);
while (noperspectivePos != String::npos) {
// + length of "\n"
source = source.replace(noperspectivePos, len_np, "");
noperspectivePos = source.find(str_np);
}
// noperspective is intentionally NOT touched here. It is core in desktop GLSL (1.30+)
// and maps to the core SPIR-V NoPerspective decoration, which DirectVulkan renders
// natively and SPIRV-Cross turns into ESSL `noperspective` + the
// GL_NV_shader_noperspective_interpolation extension. The old naked substring erase
// both discarded that interpolation (shader packs need it) and corrupted any
// identifier that merely contained the word. The GLES fallback for devices without
// the extension lives in the backend, where device capabilities are known.
FilterUnsupportedGpuShaderInt64(source);
CoerceUniformBlockPackingToStd140(source);
@@ -0,0 +1,72 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.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
#include "StripNoPerspectivePass.h"
#include "spirv.hpp"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/util/make_unique.h"
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
// OpDecorate <target-id> <decoration> [literals...]
// OpMemberDecorate <struct-id> <member> <decoration> [literals...]
constexpr uint32_t kDecorateDecorationOperand = 1;
constexpr uint32_t kMemberDecorateDecorationOperand = 2;
} // namespace
spvtools::opt::Pass::Status StripNoPerspectivePass::Process() {
auto* irContext = context();
// Collect first: KillInst mutates the annotation list being walked.
std::vector<Instruction*> toKill;
for (Instruction& annotation : irContext->annotations()) {
uint32_t decorationOperand = 0;
if (annotation.opcode() == spv::Op::OpDecorate) {
decorationOperand = kDecorateDecorationOperand;
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
decorationOperand = kMemberDecorateDecorationOperand;
} else {
continue;
}
if (annotation.NumInOperands() <= decorationOperand) {
continue;
}
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(decorationOperand)) ==
spv::Decoration::NoPerspective) {
toKill.push_back(&annotation);
}
}
if (toKill.empty()) {
return Status::SuccessWithoutChange;
}
for (Instruction* inst : toKill) {
irContext->KillInst(inst);
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken StripNoPerspectivePass::CreateStripNoPerspectivePass() {
return spvtools::Optimizer::PassToken(MakeUnique<StripNoPerspectivePass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,35 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.h
// 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
#pragma once
#include "source/opt/pass.h"
#include "spirv-tools/optimizer.hpp"
#include <Includes.h>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
// Removes the NoPerspective decoration from every interface variable and block member.
// DirectGLES fallback only, for devices that lack GL_NV_shader_noperspective_interpolation:
// SPIRV-Cross renders a NoPerspective-decorated varying as ESSL `noperspective` plus
// `#extension GL_NV_shader_noperspective_interpolation : require`, which such a driver
// rejects. Dropping the decoration falls the varying back to smooth (perspective-correct)
// interpolation - the same visible result the old text-level strip produced, but without
// corrupting identifiers and without touching DirectVulkan, where NoPerspective is native.
// (The exact screen-linear emulation via gl_Position.w / gl_FragCoord.w is a later step.)
class StripNoPerspectivePass : public spvtools::opt::Pass {
public:
const char* name() const override { return "strip-noperspective"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateStripNoPerspectivePass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL