mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Feat] (ShaderTranspiler, DirectGLES): emulate noperspective on GLES devices lacking GL_NV_shader_noperspective_interpolation - EmulateNoPerspectivePass pre-multiplies each NoPerspective output by gl_Position.w in the vertex stage and recovers each input via gl_FragCoord.w in the fragment stage (exact screen-linear L = P(a*w)*gl_FragCoord.w, handling whole-variable and component/access-chain reads, scalar and vector varyings), forces highp on emulated varyings, and strips what it cannot emulate; replaces the smooth-strip fallback so no NV extension is ever required. Restricts the vertex pre-multiply to the entry function so a non-inlined helper cannot double-scale
This commit is contained in:
@@ -193,6 +193,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
@@ -2792,13 +2792,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
// 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.
|
||||
// GL_NV_shader_noperspective_interpolation : require`; a driver without that extension
|
||||
// rejects the require. So on such devices emulate screen-linear interpolation instead
|
||||
// (pre-multiply outputs by gl_Position.w, recover inputs via gl_FragCoord.w) and drop
|
||||
// the decoration - exact, extension-free. Devices that have the extension keep the
|
||||
// decoration and let the hardware do it natively.
|
||||
Vector<unsigned int> noperspectiveSpirv;
|
||||
if (!g_GLESCapabilities.SupportsNoperspectiveInterpolation &&
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::StripNoPerspectiveForEssl(
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::EmulateNoPerspectiveForEssl(
|
||||
*effectiveSpirv, noperspectiveSpirv) &&
|
||||
!noperspectiveSpirv.empty()) {
|
||||
effectiveSpirv = &noperspectiveSpirv;
|
||||
|
||||
@@ -1257,6 +1257,185 @@ TEST_F(ProgramUtilTest, StripNoPerspectivePassRemovesBothDecorateForms) {
|
||||
<< "Location decorations must survive:\n" << outputText;
|
||||
}
|
||||
|
||||
// Phase 2 emulation - fragment side. On a device without the NV extension the NoPerspective input is
|
||||
// recovered as `load * gl_FragCoord.w` and the decoration removed; gl_FragCoord is synthesized because
|
||||
// the shader did not otherwise use it. The emulated SPIR-V must validate and decompile without the
|
||||
// extension require.
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveFragmentRecoversWithFragCoordW) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String fs = R"(#version 330 core
|
||||
noperspective in vec4 vColor;
|
||||
out vec4 f;
|
||||
void main() { f = vColor; }
|
||||
)";
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = fs};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) FAIL() << "compile: " << res.error().log;
|
||||
ProgramAttrib pa{.shaders = {res.value()}};
|
||||
auto pr = ShaderCompiler::LinkProgram(pa);
|
||||
if (!pr) FAIL() << "link: " << pr.error().log;
|
||||
ProgramBinaryAttrib ba{.shaderTypes = {GL_FRAGMENT_SHADER}, .program = *pr.value()};
|
||||
auto br = ShaderCompiler::GetSpirvBinaryFromProgram(ba);
|
||||
if (!br) FAIL() << "spirv: " << br.error().log;
|
||||
ASSERT_EQ(br.value().size(), 1u);
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(br.value()[0], emulated));
|
||||
ASSERT_FALSE(emulated.empty());
|
||||
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << "emulated SPIR-V must be valid:\n" << dis;
|
||||
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << "decoration must be stripped:\n" << dis;
|
||||
EXPECT_NE(dis.find("FragCoord"), String::npos) << "gl_FragCoord must be synthesized:\n" << dis;
|
||||
EXPECT_NE(dis.find("OpVectorTimesScalar"), String::npos) << "the recovery multiply must be present:\n" << dis;
|
||||
|
||||
SpvcSession session(emulated, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile: " << essl.error().log;
|
||||
EXPECT_EQ(essl.value().find("noperspective"), String::npos) << essl.value();
|
||||
EXPECT_EQ(essl.value().find("GL_NV_shader_noperspective_interpolation"), String::npos) << essl.value();
|
||||
EXPECT_NE(essl.value().find("gl_FragCoord"), String::npos) << "recovery must reference gl_FragCoord:\n" << essl.value();
|
||||
}
|
||||
|
||||
// Phase 2 emulation - vertex side. The NoPerspective output is pre-multiplied by gl_Position.w before
|
||||
// return and the decoration removed. Emulated SPIR-V must validate and decompile without the extension.
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveVertexPreMultipliesByPositionW) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String vs = R"(#version 330 core
|
||||
in vec4 pos;
|
||||
noperspective out vec4 vColor;
|
||||
void main() { gl_Position = pos; vColor = pos; }
|
||||
)";
|
||||
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vs};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) FAIL() << "compile: " << res.error().log;
|
||||
ProgramAttrib pa{.shaders = {res.value()}};
|
||||
auto pr = ShaderCompiler::LinkProgram(pa);
|
||||
if (!pr) FAIL() << "link: " << pr.error().log;
|
||||
ProgramBinaryAttrib ba{.shaderTypes = {GL_VERTEX_SHADER}, .program = *pr.value()};
|
||||
auto br = ShaderCompiler::GetSpirvBinaryFromProgram(ba);
|
||||
if (!br) FAIL() << "spirv: " << br.error().log;
|
||||
ASSERT_EQ(br.value().size(), 1u);
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(br.value()[0], emulated));
|
||||
ASSERT_FALSE(emulated.empty());
|
||||
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << "emulated SPIR-V must be valid:\n" << dis;
|
||||
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << "decoration must be stripped:\n" << dis;
|
||||
EXPECT_NE(dis.find("OpVectorTimesScalar"), String::npos) << "the pre-multiply must be present:\n" << dis;
|
||||
|
||||
SpvcSession session(emulated, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) FAIL() << "decompile: " << essl.error().log;
|
||||
EXPECT_EQ(essl.value().find("noperspective"), String::npos) << essl.value();
|
||||
EXPECT_NE(essl.value().find("gl_Position"), String::npos) << "pre-multiply must reference gl_Position:\n" << essl.value();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Compiles one shader stage through the full pipeline and returns its SPIR-V, or fails the test.
|
||||
MobileGL::Vector<uint32_t> CompileStageSpirv(GLenum type, const char* src) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib attrib{.shaderType = type, .sourceStr = src};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
EXPECT_TRUE(static_cast<bool>(res)) << (res ? "" : res.error().log);
|
||||
if (!res) return {};
|
||||
ProgramAttrib pa{.shaders = {res.value()}};
|
||||
auto pr = ShaderCompiler::LinkProgram(pa);
|
||||
EXPECT_TRUE(static_cast<bool>(pr)) << (pr ? "" : pr.error().log);
|
||||
if (!pr) return {};
|
||||
ProgramBinaryAttrib ba{.shaderTypes = {type}, .program = *pr.value()};
|
||||
auto br = ShaderCompiler::GetSpirvBinaryFromProgram(ba);
|
||||
EXPECT_TRUE(static_cast<bool>(br)) << (br ? "" : br.error().log);
|
||||
if (!br || br.value().empty()) return {};
|
||||
return br.value()[0];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Regression: the vertex pre-multiply must be applied exactly once (in main), not once per function.
|
||||
// glslang does not inline, so a helper function survives as its own OpFunction; instrumenting its
|
||||
// return too would scale the varying by gl_Position.w twice (w^2).
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveVertexWithHelperScalesExactlyOnce) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
// helper() returns via OpReturnValue and adds (no vector*scalar), so the ONLY OpVectorTimesScalar
|
||||
// in the module is the emulation's pre-multiply. The old all-functions code injected it at both
|
||||
// helper's and main's return -> count 2; restricted to the entry function it is 1.
|
||||
auto spirv = CompileStageSpirv(GL_VERTEX_SHADER, R"(#version 330 core
|
||||
in vec4 pos;
|
||||
noperspective out vec4 vColor;
|
||||
vec4 helper(vec4 x) { return x + vec4(1.0); }
|
||||
void main() { gl_Position = pos; vColor = helper(pos); }
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(spirv, emulated));
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << dis;
|
||||
|
||||
SizeT count = 0, off = 0;
|
||||
while ((off = dis.find("OpVectorTimesScalar", off)) != String::npos) {
|
||||
++count;
|
||||
off += std::strlen("OpVectorTimesScalar");
|
||||
}
|
||||
EXPECT_EQ(count, 1u) << "the gl_Position.w pre-multiply must happen exactly once, not per function:\n" << dis;
|
||||
}
|
||||
|
||||
// Regression: a single-component read (vColor.x), which glslang lowers via OpAccessChain, must still be
|
||||
// recovered with gl_FragCoord.w - not silently left un-scaled.
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveFragmentComponentReadIsRecovered) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
auto spirv = CompileStageSpirv(GL_FRAGMENT_SHADER, R"(#version 330 core
|
||||
noperspective in vec4 vColor;
|
||||
out vec4 f;
|
||||
void main() { f = vec4(vColor.x); }
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(spirv, emulated));
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << dis;
|
||||
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << dis;
|
||||
EXPECT_NE(dis.find("FragCoord"), String::npos)
|
||||
<< "the component read must still be recovered via gl_FragCoord.w:\n" << dis;
|
||||
}
|
||||
|
||||
// Coverage: a scalar float varying exercises the OpFMul path; a vector varying the OpVectorTimesScalar
|
||||
// path; multiple noperspective varyings in one stage are all handled.
|
||||
TEST_F(ProgramUtilTest, EmulateNoperspectiveHandlesScalarAndMultipleVaryings) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
auto spirv = CompileStageSpirv(GL_FRAGMENT_SHADER, R"(#version 330 core
|
||||
noperspective in float a;
|
||||
noperspective in vec2 b;
|
||||
out vec4 f;
|
||||
void main() { f = vec4(a, b, 1.0); }
|
||||
)");
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
|
||||
Vector<uint32_t> emulated;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateNoPerspectiveForEssl(spirv, emulated));
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String dis;
|
||||
ASSERT_TRUE(tools.Disassemble(emulated, &dis));
|
||||
ASSERT_TRUE(tools.Validate(emulated)) << dis;
|
||||
EXPECT_EQ(dis.find("NoPerspective"), String::npos) << dis;
|
||||
EXPECT_NE(dis.find("OpFMul"), String::npos) << "the scalar varying must scale with OpFMul:\n" << dis;
|
||||
EXPECT_NE(dis.find("OpVectorTimesScalar"), String::npos)
|
||||
<< "the vector varying must scale with OpVectorTimesScalar:\n" << dis;
|
||||
}
|
||||
|
||||
const char* vs_location = R"(#version 460
|
||||
|
||||
in vec4 Position;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
@@ -347,6 +348,18 @@ namespace MobileGL {
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::EmulateNoPerspectiveForEssl(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(EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass());
|
||||
|
||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary) {
|
||||
using namespace spvtools;
|
||||
|
||||
@@ -38,6 +38,11 @@ namespace MobileGL {
|
||||
// (SPIRV-Cross would otherwise require that extension and the driver would reject it).
|
||||
static bool StripNoPerspectiveForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary);
|
||||
// Emulates noperspective (screen-linear) interpolation via gl_Position.w / gl_FragCoord.w
|
||||
// so no NV extension is needed; strips what it cannot emulate. DirectGLES fallback for
|
||||
// devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass.
|
||||
static bool EmulateNoPerspectiveForEssl(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,
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.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 "EmulateNoPerspectivePass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
spv::ExecutionModel EntryExecutionModel(IRContext* ctx) {
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
return static_cast<spv::ExecutionModel>(ep.GetSingleWordInOperand(0));
|
||||
}
|
||||
return spv::ExecutionModel::Max;
|
||||
}
|
||||
|
||||
uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) {
|
||||
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id());
|
||||
// OpTypePointer <storage-class> <pointee>
|
||||
return ptrType->GetSingleWordInOperand(1);
|
||||
}
|
||||
|
||||
// If |typeId| is float or a vector of float, returns true and reports the scalar float
|
||||
// type and whether it is a vector. Matrices, structs, ints etc. are not emulatable.
|
||||
bool IsFloatScalarOrVector(IRContext* ctx, uint32_t typeId, uint32_t& floatTypeId, bool& isVector) {
|
||||
Instruction* t = ctx->get_def_use_mgr()->GetDef(typeId);
|
||||
if (t == nullptr) return false;
|
||||
if (t->opcode() == spv::Op::OpTypeFloat) {
|
||||
floatTypeId = typeId;
|
||||
isVector = false;
|
||||
return true;
|
||||
}
|
||||
if (t->opcode() == spv::Op::OpTypeVector) {
|
||||
const uint32_t comp = t->GetSingleWordInOperand(0);
|
||||
Instruction* ct = ctx->get_def_use_mgr()->GetDef(comp);
|
||||
if (ct != nullptr && ct->opcode() == spv::Op::OpTypeFloat) {
|
||||
floatTypeId = comp;
|
||||
isVector = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) {
|
||||
analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId);
|
||||
analysis::Pointer ptr(pointee, sc);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&ptr);
|
||||
}
|
||||
|
||||
uint32_t V4FloatType(IRContext* ctx) {
|
||||
analysis::Float f(32);
|
||||
analysis::Type* freg = ctx->get_type_mgr()->GetRegisteredType(&f);
|
||||
analysis::Vector v(freg, 4);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&v);
|
||||
}
|
||||
|
||||
uint32_t FloatType(IRContext* ctx) {
|
||||
analysis::Float f(32);
|
||||
return ctx->get_type_mgr()->GetTypeInstruction(&f);
|
||||
}
|
||||
|
||||
uint32_t SignedIntConstant(IRContext* ctx, int32_t value) {
|
||||
analysis::Integer i(32, true);
|
||||
analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i);
|
||||
const analysis::Constant* c =
|
||||
ctx->get_constant_mgr()->GetConstant(reg, {static_cast<uint32_t>(value)});
|
||||
return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id();
|
||||
}
|
||||
|
||||
// Multiply |valueId| (of type |valueTypeId|) by the scalar |scalarId|, inserting the op
|
||||
// before |before|. Returns the product's id.
|
||||
uint32_t InsertScale(IRContext* ctx, Instruction* before, uint32_t valueTypeId,
|
||||
uint32_t valueId, uint32_t scalarId, bool isVector) {
|
||||
const uint32_t productId = ctx->TakeNextId();
|
||||
const spv::Op op = isVector ? spv::Op::OpVectorTimesScalar : spv::Op::OpFMul;
|
||||
before->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, op, valueTypeId, productId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {valueId}},
|
||||
{SPV_OPERAND_TYPE_ID, {scalarId}}}));
|
||||
return productId;
|
||||
}
|
||||
|
||||
// --- Vertex stage: gl_Position discovery ------------------------------------------
|
||||
|
||||
// Finds gl_Position as member |memberIndex| of a gl_PerVertex-style block whose Output
|
||||
// variable is |blockVarId|; |v4floatTypeId| is that member's (vec4) type. Returns false
|
||||
// if gl_Position is not a block member (older plain-variable form is left to the strip).
|
||||
bool FindPositionBlock(IRContext* ctx, uint32_t& blockVarId, uint32_t& memberIndex,
|
||||
uint32_t& v4floatTypeId) {
|
||||
uint32_t structId = 0;
|
||||
uint32_t member = 0;
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) ==
|
||||
spv::BuiltIn::Position) {
|
||||
structId = ann.GetSingleWordInOperand(0);
|
||||
member = ann.GetSingleWordInOperand(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (structId == 0) return false;
|
||||
|
||||
Instruction* structType = ctx->get_def_use_mgr()->GetDef(structId);
|
||||
if (structType == nullptr || member >= structType->NumInOperands()) return false;
|
||||
v4floatTypeId = structType->GetSingleWordInOperand(member);
|
||||
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() == spv::Op::OpVariable &&
|
||||
static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) ==
|
||||
spv::StorageClass::Output &&
|
||||
VariablePointeeType(ctx, &inst) == structId) {
|
||||
blockVarId = inst.result_id();
|
||||
memberIndex = member;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Fragment stage: gl_FragCoord discovery/synthesis -----------------------------
|
||||
|
||||
Instruction* FindBuiltinInput(IRContext* ctx, spv::BuiltIn builtin) {
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() != spv::Op::OpDecorate || ann.NumInOperands() < 3) continue;
|
||||
if (static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::BuiltIn)
|
||||
continue;
|
||||
if (static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) != builtin) continue;
|
||||
Instruction* var = ctx->get_def_use_mgr()->GetDef(ann.GetSingleWordInOperand(0));
|
||||
if (var != nullptr && var->opcode() == spv::Op::OpVariable &&
|
||||
static_cast<spv::StorageClass>(var->GetSingleWordInOperand(0)) ==
|
||||
spv::StorageClass::Input) {
|
||||
return var;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t SynthesizeFragCoord(IRContext* ctx, uint32_t v4floatTypeId) {
|
||||
const uint32_t ptrType = PointerTypeTo(ctx, v4floatTypeId, spv::StorageClass::Input);
|
||||
const uint32_t varId = ctx->TakeNextId();
|
||||
ctx->AddGlobalValue(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpVariable, ptrType, varId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{static_cast<uint32_t>(spv::StorageClass::Input)}}}));
|
||||
ctx->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpDecorate, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {varId}},
|
||||
{SPV_OPERAND_TYPE_DECORATION,
|
||||
{static_cast<uint32_t>(spv::Decoration::BuiltIn)}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER,
|
||||
{static_cast<uint32_t>(spv::BuiltIn::FragCoord)}}}));
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
ep.AddOperand({SPV_OPERAND_TYPE_ID, {varId}});
|
||||
}
|
||||
return varId;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status EmulateNoPerspectivePass::Process() {
|
||||
auto* ctx = context();
|
||||
const spv::ExecutionModel model = EntryExecutionModel(ctx);
|
||||
const bool isVertex = model == spv::ExecutionModel::Vertex;
|
||||
const bool isFragment = model == spv::ExecutionModel::Fragment;
|
||||
|
||||
// Collect NoPerspective-decorated plain variables and every NoPerspective annotation.
|
||||
std::vector<uint32_t> plainVarIds;
|
||||
std::vector<Instruction*> decorationsToKill;
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 2 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::NoPerspective) {
|
||||
plainVarIds.push_back(ann.GetSingleWordInOperand(0));
|
||||
decorationsToKill.push_back(&ann);
|
||||
} else if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 3 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::Decoration::NoPerspective) {
|
||||
// Block-member noperspective is not emulated here; the decoration is stripped
|
||||
// (smooth fallback) so SPIRV-Cross does not require the NV extension.
|
||||
decorationsToKill.push_back(&ann);
|
||||
}
|
||||
}
|
||||
|
||||
if (decorationsToKill.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
const spv::StorageClass wantStorage =
|
||||
isVertex ? spv::StorageClass::Output : spv::StorageClass::Input;
|
||||
|
||||
// Emulatable = plain variable of the stage's interface direction, float or floatN.
|
||||
struct Target {
|
||||
Instruction* var;
|
||||
uint32_t typeId;
|
||||
uint32_t floatTypeId;
|
||||
bool isVector;
|
||||
};
|
||||
std::vector<Target> targets;
|
||||
if (isVertex || isFragment) {
|
||||
for (const uint32_t id : plainVarIds) {
|
||||
Instruction* var = ctx->get_def_use_mgr()->GetDef(id);
|
||||
if (var == nullptr || var->opcode() != spv::Op::OpVariable) continue;
|
||||
if (static_cast<spv::StorageClass>(var->GetSingleWordInOperand(0)) != wantStorage)
|
||||
continue;
|
||||
const uint32_t pointee = VariablePointeeType(ctx, var);
|
||||
uint32_t floatTypeId = 0;
|
||||
bool isVector = false;
|
||||
if (IsFloatScalarOrVector(ctx, pointee, floatTypeId, isVector)) {
|
||||
targets.push_back({var, pointee, floatTypeId, isVector});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force highp on the varyings we emulate: the a*w round-trip overflows a mediump (fp16)
|
||||
// varying at large clip-space w. Dropping RelaxedPrecision makes SPIRV-Cross emit them
|
||||
// highp on both stages, keeping the emulation exact. Only touches emulated variables.
|
||||
if (!targets.empty()) {
|
||||
std::vector<uint32_t> targetIds;
|
||||
targetIds.reserve(targets.size());
|
||||
for (const Target& t : targets) targetIds.push_back(t.var->result_id());
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 2 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::RelaxedPrecision &&
|
||||
std::find(targetIds.begin(), targetIds.end(),
|
||||
ann.GetSingleWordInOperand(0)) != targetIds.end()) {
|
||||
decorationsToKill.push_back(&ann);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isVertex && !targets.empty()) {
|
||||
uint32_t blockVarId = 0;
|
||||
uint32_t memberIndex = 0;
|
||||
uint32_t v4floatTypeId = 0;
|
||||
if (FindPositionBlock(ctx, blockVarId, memberIndex, v4floatTypeId)) {
|
||||
const uint32_t ptrOutV4 =
|
||||
PointerTypeTo(ctx, v4floatTypeId, spv::StorageClass::Output);
|
||||
const uint32_t memberConst = SignedIntConstant(ctx, static_cast<int32_t>(memberIndex));
|
||||
const uint32_t floatTy = FloatType(ctx);
|
||||
|
||||
uint32_t entryFuncId = 0;
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
// OpEntryPoint <model> <function> "name" <interface...>
|
||||
entryFuncId = ep.GetSingleWordInOperand(1);
|
||||
break;
|
||||
}
|
||||
|
||||
// Pre-multiply every target output by gl_Position.w before each return of the
|
||||
// ENTRY function only. glslang does not inline, so a called helper survives as
|
||||
// its own OpFunction; instrumenting its returns too would scale the varying
|
||||
// more than once (w^2), breaking the identity.
|
||||
for (auto funcIt = ctx->module()->begin(); funcIt != ctx->module()->end(); ++funcIt) {
|
||||
if (funcIt->result_id() != entryFuncId) continue;
|
||||
funcIt->ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() != spv::Op::OpReturn &&
|
||||
inst->opcode() != spv::Op::OpReturnValue) {
|
||||
return;
|
||||
}
|
||||
const uint32_t posPtrId = ctx->TakeNextId();
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpAccessChain, ptrOutV4, posPtrId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {blockVarId}},
|
||||
{SPV_OPERAND_TYPE_ID, {memberConst}}}));
|
||||
const uint32_t posId = ctx->TakeNextId();
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, v4floatTypeId, posId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {posPtrId}}}));
|
||||
const uint32_t wId = ctx->TakeNextId();
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpCompositeExtract, floatTy, wId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {posId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {3u}}}));
|
||||
for (const Target& t : targets) {
|
||||
const uint32_t valId = ctx->TakeNextId();
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, t.typeId, valId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {t.var->result_id()}}}));
|
||||
const uint32_t scaledId =
|
||||
InsertScale(ctx, inst, t.typeId, valId, wId, t.isVector);
|
||||
inst->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpStore, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {t.var->result_id()}},
|
||||
{SPV_OPERAND_TYPE_ID, {scaledId}}}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isFragment && !targets.empty()) {
|
||||
Instruction* fragCoord = FindBuiltinInput(ctx, spv::BuiltIn::FragCoord);
|
||||
uint32_t fragCoordId = 0;
|
||||
uint32_t v4floatTypeId = 0;
|
||||
if (fragCoord != nullptr) {
|
||||
fragCoordId = fragCoord->result_id();
|
||||
v4floatTypeId = VariablePointeeType(ctx, fragCoord);
|
||||
} else {
|
||||
v4floatTypeId = V4FloatType(ctx);
|
||||
fragCoordId = SynthesizeFragCoord(ctx, v4floatTypeId);
|
||||
}
|
||||
const uint32_t floatTy = FloatType(ctx);
|
||||
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
for (const Target& t : targets) {
|
||||
// Collect every load that reads the varying. glslang lowers a whole-variable
|
||||
// read to OpLoad(var), but a single-component read (v.x) to
|
||||
// OpAccessChain(var) + OpLoad(chain). Both must be scaled; the identity is
|
||||
// per-component, so scaling one loaded component by gl_FragCoord.w is valid.
|
||||
std::vector<Instruction*> loads;
|
||||
defUse->ForEachUser(t.var, [&](Instruction* user) {
|
||||
if (user->opcode() == spv::Op::OpLoad &&
|
||||
user->GetSingleWordInOperand(0) == t.var->result_id()) {
|
||||
loads.push_back(user);
|
||||
} else if (user->opcode() == spv::Op::OpAccessChain &&
|
||||
user->GetSingleWordInOperand(0) == t.var->result_id()) {
|
||||
const uint32_t chainId = user->result_id();
|
||||
defUse->ForEachUser(user, [&](Instruction* chainUser) {
|
||||
if (chainUser->opcode() == spv::Op::OpLoad &&
|
||||
chainUser->GetSingleWordInOperand(0) == chainId) {
|
||||
loads.push_back(chainUser);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Rewrite `%r = OpLoad %ty %ptr` into
|
||||
// %orig = OpLoad %ty %ptr
|
||||
// %fc = OpLoad %v4float %fragCoord
|
||||
// %w = OpCompositeExtract %float %fc 3
|
||||
// %r = OpVectorTimesScalar/OpFMul %ty %orig %w (reuse %r: uses stay intact)
|
||||
// The op is chosen from the LOAD's own result type: a whole-vector load scales
|
||||
// with OpVectorTimesScalar, a scalar component load with OpFMul.
|
||||
for (Instruction* load : loads) {
|
||||
const uint32_t loadType = load->type_id();
|
||||
uint32_t componentFloat = 0;
|
||||
bool loadIsVector = false;
|
||||
if (!IsFloatScalarOrVector(ctx, loadType, componentFloat, loadIsVector)) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t ptrId = load->GetSingleWordInOperand(0);
|
||||
const uint32_t origId = ctx->TakeNextId();
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, loadType, origId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {ptrId}}}));
|
||||
const uint32_t fcId = ctx->TakeNextId();
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLoad, v4floatTypeId, fcId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {fragCoordId}}}));
|
||||
const uint32_t wId = ctx->TakeNextId();
|
||||
load->InsertBefore(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpCompositeExtract, floatTy, wId,
|
||||
std::initializer_list<Operand>{{SPV_OPERAND_TYPE_ID, {fcId}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {3u}}}));
|
||||
load->SetOpcode(loadIsVector ? spv::Op::OpVectorTimesScalar : spv::Op::OpFMul);
|
||||
load->SetInOperands(Instruction::OperandList{
|
||||
{SPV_OPERAND_TYPE_ID, {origId}}, {SPV_OPERAND_TYPE_ID, {wId}}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip every NoPerspective decoration: emulated varyings now transport smooth, and
|
||||
// non-emulatable ones fall back to smooth.
|
||||
for (Instruction* dec : decorationsToKill) {
|
||||
ctx->KillInst(dec);
|
||||
}
|
||||
|
||||
ctx->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken EmulateNoPerspectivePass::CreateEmulateNoPerspectivePass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<EmulateNoPerspectivePass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,41 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.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 {
|
||||
// Emulates 'noperspective' (screen-linear) interpolation on GLES devices that lack
|
||||
// GL_NV_shader_noperspective_interpolation, so no NV extension is required. The hardware
|
||||
// interpolates perspective-correct; screen-linear L(a) is recovered from the identity
|
||||
// L(a) = P(a * w) * gl_FragCoord.w
|
||||
// where P is perspective-correct interpolation and w is the vertex clip-space w. So each
|
||||
// NoPerspective-decorated output is pre-multiplied by gl_Position.w in the vertex stage
|
||||
// and each NoPerspective-decorated input is multiplied by gl_FragCoord.w in the fragment
|
||||
// stage; the decoration is then removed so the varying transports smooth. This is exact
|
||||
// (modulo float precision - the emulated varyings want highp).
|
||||
//
|
||||
// Scope: plain interface variables of float or floatN type. Anything it cannot emulate
|
||||
// (interface-block members, matrices, or a stage lacking the needed builtin) has its
|
||||
// NoPerspective decoration stripped instead, degrading to smooth - the same result the
|
||||
// extension-less fallback produced before, and never invalid SPIR-V. DirectGLES only.
|
||||
class EmulateNoPerspectivePass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "emulate-noperspective"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateEmulateNoPerspectivePass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user