mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Feat, Test] (ShaderTranspiler, MG_Test): demote gl_ViewportIndex to a plain global for ESSL targets
This commit is contained in:
@@ -279,6 +279,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
|
||||
@@ -9,6 +9,7 @@ add_executable(
|
||||
EmulateSubgroupsTest.cpp
|
||||
DemoteFloat64Test.cpp
|
||||
FlattenXfbInterfaceBlocksTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(SpirvPassTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LowerViewportIndexTest.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
|
||||
//
|
||||
// LowerViewportIndexPass is the DirectGLES fallback for a driver with no GL_OES_viewport_array.
|
||||
// The thing it prevents is not a wrong pixel but a missing program: ESSL has no core
|
||||
// gl_ViewportIndex at any version, SPIRV-Cross prints the identifier bare, and the driver rejects
|
||||
// the stage - after which DirectGLES binds program 0 and every draw renders nothing while
|
||||
// GL_LINK_STATUS still answers TRUE. So what has to hold is textual and structural at once: the
|
||||
// emitted ESSL must stop naming the builtin, the module must stay valid, and gl_Layer - which IS
|
||||
// core in ESSL 3.20 geometry shaders - must come through untouched.
|
||||
//
|
||||
// Real GLSL through the same glslang path the backends use, rather than hand-assembled words, for
|
||||
// the same reason MG_Test/Pipeline/ViewportIndexReflectionTest.cpp does it: what matters is what
|
||||
// glslang actually emits for these shaders.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||
|
||||
namespace {
|
||||
Vector<Uint32> CompileToSpirv(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
String Disassemble(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String text;
|
||||
tools.Disassemble(spirv, &text);
|
||||
return text;
|
||||
}
|
||||
|
||||
// ESSL 320, i.e. exactly what the DirectGLES transpile asks SPIRV-Cross for.
|
||||
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{};
|
||||
}
|
||||
|
||||
Bool Contains(const String& haystack, const String& needle) {
|
||||
return haystack.find(needle) != String::npos;
|
||||
}
|
||||
|
||||
// KHR-GL4x.viewport_array.draw_to_single_layer_with_multiple_viewports' geometry stage in
|
||||
// miniature: sixteen invocations, each routing its primitive to its own viewport. This is the
|
||||
// shape that today loses the whole program on a driver without GL_OES_viewport_array.
|
||||
const char* const kGeometryWritesViewportIndex = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// Layered rendering, not viewport routing. gl_Layer IS core in ESSL 3.20 geometry shaders, so
|
||||
// demoting it would break a Minecraft-style cubemap pass that works today.
|
||||
const char* const kGeometryWritesLayerOnly = R"(#version 410 core
|
||||
layout(points, invocations = 6) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_Layer = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// Both at once, which is the case that separates "lowers the right builtin" from "lowers every
|
||||
// builtin it can reach": KHR-GL4x.viewport_array.draw_multiple_layers writes both.
|
||||
const char* const kGeometryWritesBoth = R"(#version 410 core
|
||||
layout(points, invocations = 16) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_ViewportIndex = gl_InvocationID;
|
||||
gl_Layer = gl_InvocationID;
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kPlainGeometry = R"(#version 410 core
|
||||
layout(points, invocations = 1) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
void main() {
|
||||
gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, -1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); EmitVertex();
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
class LowerViewportIndexTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
MobileGL::Initialize();
|
||||
m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart)
|
||||
<< "the lowered module did not survive spirv-val";
|
||||
}
|
||||
|
||||
Uint64 m_validationFailuresAtStart = 0;
|
||||
};
|
||||
|
||||
// The probe is the gate that keeps every ordinary stage off an optimizer round trip, so it has to
|
||||
// answer no for a shader that never routes a viewport - and yes for the one that does.
|
||||
TEST_F(LowerViewportIndexTest, TheProbeAnswersOnlyForAViewportIndexWriter) {
|
||||
const Vector<Uint32> plain = CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry);
|
||||
ASSERT_FALSE(plain.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin(plain));
|
||||
|
||||
const Vector<Uint32> layerOnly = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesLayerOnly);
|
||||
ASSERT_FALSE(layerOnly.empty());
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin(layerOnly));
|
||||
|
||||
const Vector<Uint32> writer = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex);
|
||||
ASSERT_FALSE(writer.empty());
|
||||
EXPECT_TRUE(ShaderCompiler::DeclaresViewportIndexBuiltin(writer));
|
||||
|
||||
// Runs on every stage of every program on a driver without the extension, so it must survive a
|
||||
// stage that produced no SPIR-V rather than pushing a parse diagnostic for it.
|
||||
EXPECT_FALSE(ShaderCompiler::DeclaresViewportIndexBuiltin({}));
|
||||
}
|
||||
|
||||
// The whole point: the emitted ESSL must stop naming a builtin the language does not have.
|
||||
TEST_F(LowerViewportIndexTest, DemotesTheBuiltinToAnOrdinaryGlobal) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesViewportIndex);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
// Negative control, and the bug itself: untouched, SPIRV-Cross prints gl_ViewportIndex into
|
||||
// ESSL 320 and asks for no extension to go with it.
|
||||
const String before = Transpile(input);
|
||||
EXPECT_TRUE(Contains(before, "gl_ViewportIndex")) << before;
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
ASSERT_TRUE(tools.Validate(output)) << dis;
|
||||
EXPECT_FALSE(Contains(dis, "BuiltIn ViewportIndex")) << dis;
|
||||
EXPECT_TRUE(Contains(dis, "mg_ViewportIndex")) << dis;
|
||||
EXPECT_TRUE(Contains(dis, "Private")) << dis;
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_TRUE(Contains(after, "mg_ViewportIndex")) << after;
|
||||
EXPECT_FALSE(Contains(after, "gl_ViewportIndex")) << after;
|
||||
}
|
||||
|
||||
// gl_Layer is core in ESSL 3.20 geometry shaders and layered rendering works on this backend
|
||||
// today. Lowering it too would trade one silent failure for another.
|
||||
TEST_F(LowerViewportIndexTest, LeavesGlLayerAlone) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kGeometryWritesBoth);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
ASSERT_TRUE(tools.Validate(output)) << dis;
|
||||
EXPECT_FALSE(Contains(dis, "BuiltIn ViewportIndex")) << dis;
|
||||
EXPECT_TRUE(Contains(dis, "BuiltIn Layer")) << dis;
|
||||
|
||||
const String after = Transpile(output);
|
||||
EXPECT_TRUE(Contains(after, "gl_Layer")) << after;
|
||||
EXPECT_FALSE(Contains(after, "gl_ViewportIndex")) << after;
|
||||
}
|
||||
|
||||
// Every other stage on a driver without the extension goes through this pass too (behind the
|
||||
// probe), so a module it has nothing to do with must come out saying exactly what it said.
|
||||
TEST_F(LowerViewportIndexTest, LeavesAModuleWithoutTheBuiltinUntouched) {
|
||||
const Vector<Uint32> input = CompileToSpirv(GL_GEOMETRY_SHADER, kPlainGeometry);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
const String before = Transpile(input);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LowerViewportIndexForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
|
||||
const String dis = Disassemble(output);
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
ASSERT_TRUE(tools.Validate(output)) << dis;
|
||||
EXPECT_FALSE(Contains(dis, "mg_ViewportIndex")) << dis;
|
||||
EXPECT_EQ(Transpile(output), before);
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "SpirvPasses/DecoratePositionInvariantPass.h"
|
||||
#include "SpirvPasses/DemoteFloat64Pass.h"
|
||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||
#include "SpirvPasses/LowerViewportIndexPass.h"
|
||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
|
||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||
@@ -635,6 +636,21 @@ namespace MobileGL {
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::LowerViewportIndexForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(LowerViewportIndexPass::CreateLowerViewportIndexPass());
|
||||
|
||||
return RunOptimizerChecked("LowerViewportIndexForEssl", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary) {
|
||||
return LowerViewportIndexPass::DeclaresViewportIndexBuiltin(binary);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
const bool enableSpirvValidation) {
|
||||
|
||||
@@ -32,6 +32,20 @@ namespace MobileGL {
|
||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Demotes the gl_ViewportIndex OUTPUT builtin to a plain Private global named
|
||||
// mg_ViewportIndex, so SPIRV-Cross emits an ordinary declaration instead of a bare
|
||||
// gl_ViewportIndex that ESSL has no core spelling for. Multi-viewport routing is
|
||||
// lost (everything lands in viewport 0) but the stage compiles and the program
|
||||
// runs, instead of every draw made with it becoming a silent no-op. Only for the
|
||||
// DirectGLES transpile path on a driver WITHOUT GL_OES_viewport_array; gl_Layer is
|
||||
// deliberately left alone, being core in ESSL 3.20 geometry shaders.
|
||||
static bool LowerViewportIndexForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Whether the module declares an output decorated BuiltIn ViewportIndex, i.e.
|
||||
// whether the pass above has anything to do. The gate that keeps every other
|
||||
// stage off an optimizer round trip it does not need.
|
||||
static bool DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary);
|
||||
// Replaces an ARRAY vertex input with one input per element at consecutive
|
||||
// locations, seeding a Private copy of the array so indexed reads still work.
|
||||
// GLSL ES has no array vertex inputs and SPIRV-Cross refuses the whole module
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.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 "LowerViewportIndexPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/build_module.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/util/make_unique.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
|
||||
// The name the decompiled ESSL ends up declaring. Same mg_ prefix as the
|
||||
// draw-parameter lowering, so a global that came from a demoted builtin is
|
||||
// recognisable in a driver log.
|
||||
constexpr const char* kLoweredName = "mg_ViewportIndex";
|
||||
|
||||
// The one decoration this pass lowers. OpDecorate only, never OpMemberDecorate:
|
||||
// glslang emits gl_ViewportIndex as a standalone variable, and a member of a
|
||||
// gl_PerVertex-shaped block could not be demoted on its own anyway. BuiltIn Layer
|
||||
// is deliberately not matched - see the header.
|
||||
Bool IsViewportIndexBuiltinDecoration(const Instruction& annotation) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate ||
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
|
||||
spv::Decoration::BuiltIn) {
|
||||
return false;
|
||||
}
|
||||
return static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) ==
|
||||
spv::BuiltIn::ViewportIndex;
|
||||
}
|
||||
|
||||
// The OUTPUT variable that decoration names, or nullptr. Only an output is
|
||||
// demotable: a fragment stage READS gl_ViewportIndex as an Input, and a Private
|
||||
// global has no defined value to read, so lowering that one would answer the
|
||||
// shader with garbage instead of the viewport it asked for. That case is left for
|
||||
// the driver to reject.
|
||||
Instruction* GetDecoratedViewportIndexOutput(IRContext* context,
|
||||
const Instruction& annotation) {
|
||||
Instruction* variable =
|
||||
context->get_def_use_mgr()->GetDef(annotation.GetSingleWordInOperand(0));
|
||||
if (variable == nullptr || variable->opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Output) {
|
||||
return nullptr;
|
||||
}
|
||||
return variable;
|
||||
}
|
||||
|
||||
// Decorations the validator accepts only on an Input/Output variable, so they have
|
||||
// to go with the storage class or the demoted module stops validating. glslang
|
||||
// puts none of these on gl_ViewportIndex today - the BuiltIn is all it writes -
|
||||
// but a geometry `layout(stream = N)` qualifier decorates every output of the
|
||||
// stage, and the pass must not be the thing that produces an invalid module.
|
||||
Bool IsInterfaceOnlyDecoration(spv::Decoration decoration) {
|
||||
switch (decoration) {
|
||||
case spv::Decoration::Flat:
|
||||
case spv::Decoration::NoPerspective:
|
||||
case spv::Decoration::Centroid:
|
||||
case spv::Decoration::Sample:
|
||||
case spv::Decoration::Patch:
|
||||
case spv::Decoration::Invariant:
|
||||
case spv::Decoration::Location:
|
||||
case spv::Decoration::Component:
|
||||
case spv::Decoration::Stream:
|
||||
case spv::Decoration::XfbBuffer:
|
||||
case spv::Decoration::XfbStride:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void ReplaceName(IRContext* context, uint32_t id, const char* name) {
|
||||
for (auto& debugInst : context->debugs2()) {
|
||||
if (debugInst.opcode() == spv::Op::OpName && debugInst.GetSingleWordInOperand(0) == id) {
|
||||
debugInst.SetInOperand(
|
||||
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(name));
|
||||
return;
|
||||
}
|
||||
}
|
||||
context->AddDebug2Inst(spvtools::MakeUnique<Instruction>(
|
||||
context, spv::Op::OpName, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {id}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}}));
|
||||
}
|
||||
|
||||
void RemoveFromEntryPointInterfaces(IRContext* context, uint32_t id) {
|
||||
for (Instruction& entryPoint : context->module()->entry_points()) {
|
||||
std::vector<Operand> newOperands;
|
||||
Bool changed = false;
|
||||
for (uint32_t i = 0; i < entryPoint.NumInOperands(); ++i) {
|
||||
const Operand& operand = entryPoint.GetInOperand(i);
|
||||
// Interface ids start after execution model, entry-point id and name.
|
||||
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID &&
|
||||
entryPoint.GetSingleWordInOperand(i) == id) {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
newOperands.push_back(operand);
|
||||
}
|
||||
if (changed) {
|
||||
entryPoint.SetInOperands(std::move(newOperands));
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool LowerViewportIndexPass::DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary) {
|
||||
if (binary.empty()) {
|
||||
// An empty module is a stage that produced no SPIR-V, which is not a verdict
|
||||
// about viewport routing; letting BuildModule reject it would push a spurious
|
||||
// diagnostic through the message consumer first.
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<IRContext> context = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
||||
binary.data(), binary.size());
|
||||
if (!context) {
|
||||
// Unparseable here means unusable downstream too; let the ordinary transpile
|
||||
// path produce the error rather than inventing a verdict from it.
|
||||
return false;
|
||||
}
|
||||
for (const Instruction& annotation : context->annotations()) {
|
||||
if (IsViewportIndexBuiltinDecoration(annotation) &&
|
||||
GetDecoratedViewportIndexOutput(context.get(), annotation) != nullptr) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status LowerViewportIndexPass::Process() {
|
||||
auto* irContext = context();
|
||||
|
||||
// Collect the decorations to lower first; mutating while iterating annotations
|
||||
// invalidates the range.
|
||||
struct LoweredVariable {
|
||||
Instruction* variable = nullptr;
|
||||
Instruction* decoration = nullptr;
|
||||
};
|
||||
std::vector<LoweredVariable> targets;
|
||||
|
||||
for (auto& annotation : irContext->annotations()) {
|
||||
if (!IsViewportIndexBuiltinDecoration(annotation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* variable = GetDecoratedViewportIndexOutput(irContext, annotation);
|
||||
if (variable == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
targets.push_back({variable, &annotation});
|
||||
}
|
||||
|
||||
if (targets.empty()) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// Second collection pass, for the same reason as the first: the decorations that
|
||||
// stop being legal once the variable leaves the Output storage class.
|
||||
std::vector<Instruction*> deadDecorations;
|
||||
for (auto& annotation : irContext->annotations()) {
|
||||
if (annotation.opcode() != spv::Op::OpDecorate ||
|
||||
!IsInterfaceOnlyDecoration(
|
||||
static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)))) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t decoratedId = annotation.GetSingleWordInOperand(0);
|
||||
for (const auto& target : targets) {
|
||||
if (target.variable->result_id() == decoratedId) {
|
||||
deadDecorations.push_back(&annotation);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto* defUseMgr = irContext->get_def_use_mgr();
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
|
||||
for (auto& target : targets) {
|
||||
Instruction* variable = target.variable;
|
||||
const uint32_t variableId = variable->result_id();
|
||||
|
||||
// Demote the Output builtin to a plain Private global. Every store the shader
|
||||
// already makes stays exactly where it is - it simply no longer reaches the
|
||||
// rasterizer, which is the whole of the degradation.
|
||||
Instruction* pointerType = defUseMgr->GetDef(variable->type_id());
|
||||
const uint32_t pointeeTypeId = pointerType->GetSingleWordInOperand(1);
|
||||
const uint32_t privatePointerTypeId =
|
||||
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Private);
|
||||
variable->SetResultType(privatePointerTypeId);
|
||||
variable->SetInOperand(0, {static_cast<uint32_t>(spv::StorageClass::Private)});
|
||||
|
||||
irContext->KillInst(target.decoration);
|
||||
RemoveFromEntryPointInterfaces(irContext, variableId);
|
||||
ReplaceName(irContext, variableId, kLoweredName);
|
||||
}
|
||||
|
||||
for (auto* decoration : deadDecorations) {
|
||||
irContext->KillInst(decoration);
|
||||
}
|
||||
|
||||
// The MultiViewport / ShaderViewportIndexLayerEXT capabilities are deliberately
|
||||
// left declared, unlike DrawParameters in the sibling pass. They are not exclusive
|
||||
// to this builtin: ShaderViewportIndexLayerEXT also enables gl_Layer in the
|
||||
// pre-geometry stages, and it DEPENDS on MultiViewport, so dropping either can
|
||||
// invalidate a module that still writes Layer. A declared-but-unused capability is
|
||||
// legal SPIR-V and SPIRV-Cross's GLSL backend reads neither of them, so leaving
|
||||
// both costs nothing.
|
||||
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken LowerViewportIndexPass::CreateLowerViewportIndexPass() {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<LowerViewportIndexPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,51 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.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 {
|
||||
// ESSL has no core gl_ViewportIndex at any version - only GL_OES_viewport_array
|
||||
// introduces it - and SPIRV-Cross prints the identifier bare, requesting no extension
|
||||
// for it (contrast BuiltInLayer, which it backs with GL_NV_viewport_array2 on ES). On
|
||||
// a driver WITHOUT that extension the stage therefore fails to compile, DirectGLES
|
||||
// marks the program unusable and binds program 0 for it, and every draw silently
|
||||
// renders nothing while GL_LINK_STATUS still answers TRUE - the failure signature
|
||||
// KHR-GL4x.viewport_array reports as "expected N, got -1", i.e. the untouched upload.
|
||||
//
|
||||
// This pass demotes the ViewportIndex OUTPUT to a plain Private global named
|
||||
// mg_ViewportIndex, so the decompiled ESSL declares an ordinary global the shader
|
||||
// still writes and nothing reads. The program compiles and rendering degrades to
|
||||
// viewport 0 - which is the single-viewport behaviour MG_IntegrationTest's
|
||||
// ViewportArrayScenario already documents for this backend - instead of the whole
|
||||
// program becoming a no-op. Only meant for the DirectGLES transpile path; the Vulkan
|
||||
// backend keeps the native builtin and routes it for real.
|
||||
//
|
||||
// gl_Layer is deliberately NOT touched: BuiltIn Layer IS core in ESSL 3.20 geometry
|
||||
// shaders, and demoting it would break layered rendering that works today.
|
||||
class LowerViewportIndexPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "lower-viewport-index"; }
|
||||
Status Process() override;
|
||||
|
||||
// Whether the module declares an output decorated BuiltIn ViewportIndex, i.e.
|
||||
// whether running this pass could change anything. Answered from a single parse so
|
||||
// the caller can skip the optimizer round trip entirely - which is every shader
|
||||
// but the handful that route viewports from the shader.
|
||||
static bool DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateLowerViewportIndexPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user