mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +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/DecoratePositionInvariantPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.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/PackDoubleVertexInputsPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ add_executable(
|
|||||||
EmulateSubgroupsTest.cpp
|
EmulateSubgroupsTest.cpp
|
||||||
DemoteFloat64Test.cpp
|
DemoteFloat64Test.cpp
|
||||||
FlattenXfbInterfaceBlocksTest.cpp
|
FlattenXfbInterfaceBlocksTest.cpp
|
||||||
|
LowerViewportIndexTest.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(SpirvPassTest PRIVATE
|
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/DecoratePositionInvariantPass.h"
|
||||||
#include "SpirvPasses/DemoteFloat64Pass.h"
|
#include "SpirvPasses/DemoteFloat64Pass.h"
|
||||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||||
|
#include "SpirvPasses/LowerViewportIndexPass.h"
|
||||||
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
#include "SpirvPasses/PackDoubleVertexInputsPass.h"
|
||||||
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
|
#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h"
|
||||||
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
|
||||||
@@ -635,6 +636,21 @@ namespace MobileGL {
|
|||||||
outputBinary, true, enableSpirvValidation);
|
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,
|
bool ShaderCompiler::SplitArrayVertexInputsForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
const bool enableSpirvValidation) {
|
const bool enableSpirvValidation) {
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ namespace MobileGL {
|
|||||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary,
|
Vector<uint32_t>& outputBinary,
|
||||||
bool enableSpirvValidation = false);
|
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
|
// 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.
|
// 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
|
// 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