[Fix] (Magma): lower pixel_center_integer gl_FragCoord so Vulkan validation stops refusing the program

This commit is contained in:
2026-10-12 15:06:42 -04:00
parent 171eed9ac0
commit da6b172d08
8 changed files with 391 additions and 0 deletions
+1
View File
@@ -416,6 +416,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
@@ -3675,6 +3675,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
// Vulkan forbids the PixelCenterInteger execution mode that
// `layout(pixel_center_integer) in vec4 gl_FragCoord;` compiles to, so a module
// carrying it would fail validation and every draw of the program be refused. This
// runs before the FragCoordYFlip variant: the flip then rewrites the unshifted builtin
// read and the -0.5 lands on the flipped value, as GL defines it.
if (stages[i] == ShaderStage::Fragment) {
Vector<Uint> centeredSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerPixelCenterIntegerForVulkan(
moduleSpirvs[i], centeredSpirv, enableSpirvValidation) &&
!centeredSpirv.empty()) {
moduleSpirvs[i] = Move(centeredSpirv);
}
}
if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && stages[i] == ShaderStage::Fragment) {
Vector<Uint> fragCoordSpirv;
if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) {
@@ -20,6 +20,7 @@ add_executable(
GlslangCaptureTest.cpp
DemotePointSizeTest.cpp
FragCoordYFlipTest.cpp
PixelCenterIntegerTest.cpp
RelaxedOpaqueStructTest.cpp
ShadingRateEsslTest.cpp
)
@@ -0,0 +1,131 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/PixelCenterIntegerTest.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
// `layout(pixel_center_integer) in vec4 gl_FragCoord;` compiles to the PixelCenterInteger
// execution mode, which the Vulkan environment forbids: DirectVulkan's validation refused every
// such program (KHR-GL45.direct_state_access.textures_* draw through one). The lowered module
// must validate, carry no such mode, and shift exactly the x/y reads of the builtin.
#include <gtest/gtest.h>
#include "Includes.h"
#include "Init.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include "spirv-tools/libspirv.hpp"
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
Vector<Uint32> CompileFragment(const char* fragment) {
using namespace MG_Util::ShaderTranspiler;
constexpr const char* kVertex = R"(#version 330 core
layout(location = 0) in vec2 position;
void main() { gl_Position = vec4(position, 0.0, 1.0); }
)";
Vector<SharedPtr<glslang::TShader>> shaders;
for (const auto& [stage, source] : Vector<Pair<GLenum, const char*>>{{GL_VERTEX_SHADER, kVertex},
{GL_FRAGMENT_SHADER, fragment}}) {
ShaderAttrib attrib{.shaderType = stage, .sourceStr = source};
auto shader = ShaderCompiler::CompileShader(attrib);
EXPECT_TRUE(shader) << (shader ? String{} : shader.error().log);
if (!shader) return {};
shaders.push_back(shader.value());
}
ProgramAttrib programAttrib{.shaders = shaders};
auto program = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(program) << (program ? String{} : program.error().log);
if (!program) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER},
.program = *program.value()};
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binary) << (binary ? String{} : binary.error().log);
if (!binary || binary->size() != 2) return {};
Vector<Uint32> module = binary.value()[1];
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
return module;
}
String Disassemble(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String text;
EXPECT_TRUE(tools.Disassemble(spirv, &text));
return text;
}
String ValidationError(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
String error;
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&,
const char* message) { error += message != nullptr ? message : ""; });
return tools.Validate(spirv) ? String{} : (error.empty() ? String{"invalid"} : error);
}
SizeT CountOccurrences(const String& text, const String& needle) {
SizeT count = 0;
for (SizeT at = text.find(needle); at != String::npos; at = text.find(needle, at + needle.size())) ++count;
return count;
}
} // namespace
// The direct_state_access shape: the whole vector converted to integer texel coordinates.
TEST(LowerPixelCenterInteger, ShiftsWholeVectorReadsAndDropsTheMode) {
MobileGL::Initialize();
const Vector<Uint32> input = CompileFragment(R"(#version 330 core
layout(pixel_center_integer) in vec4 gl_FragCoord;
uniform sampler2D data;
out vec4 color;
void main() { color = texelFetch(data, ivec2(gl_FragCoord.xy), 0); }
)");
ASSERT_FALSE(input.empty());
ASSERT_NE(Disassemble(input).find("PixelCenterInteger"), String::npos);
EXPECT_FALSE(ValidationError(input).empty()) << "the unlowered module is what Vulkan refuses";
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LowerPixelCenterIntegerForVulkan(input, output, true));
EXPECT_EQ(ValidationError(output), String{});
const String text = Disassemble(output);
EXPECT_EQ(text.find("PixelCenterInteger"), String::npos) << text;
EXPECT_EQ(CountOccurrences(text, "OpFSub"), 1u) << text;
EXPECT_NE(text.find("OpConstantComposite %v4float %float_0_5 %float_0_5 %float_0 %float_0"), String::npos)
<< text;
}
// Single components arrive through an access chain: x and y move, z and w keep their values.
TEST(LowerPixelCenterInteger, ShiftsOnlyTheXAndYComponents) {
MobileGL::Initialize();
const Vector<Uint32> input = CompileFragment(R"(#version 330 core
layout(pixel_center_integer) in vec4 gl_FragCoord;
out vec4 color;
void main() { color = vec4(gl_FragCoord.x, gl_FragCoord.y, gl_FragCoord.z, gl_FragCoord.w); }
)");
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LowerPixelCenterIntegerForVulkan(input, output, true));
EXPECT_EQ(ValidationError(output), String{});
const String text = Disassemble(output);
EXPECT_EQ(text.find("PixelCenterInteger"), String::npos) << text;
EXPECT_EQ(CountOccurrences(text, "OpFSub"), 2u) << text;
}
// A module without the mode is left alone.
TEST(LowerPixelCenterInteger, LeavesDefaultCentredModulesUnchanged) {
MobileGL::Initialize();
const Vector<Uint32> input = CompileFragment(R"(#version 330 core
out vec4 color;
void main() { color = vec4(gl_FragCoord.xy, 0.0, 1.0); }
)");
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LowerPixelCenterIntegerForVulkan(input, output, true));
EXPECT_EQ(Disassemble(output), Disassemble(input));
}
@@ -33,6 +33,7 @@
#include "SpirvPasses/SplitArrayVertexInputsPass.h"
#include "SpirvPasses/RebaseInstanceIndexPass.h"
#include "SpirvPasses/ZeroBaseVertexPass.h"
#include "SpirvPasses/LowerPixelCenterIntegerPass.h"
#include "SpirvPasses/DeriveNumSubgroupsPass.h"
#include "SpirvPasses/EmulateSubgroupsPass.h"
#include "SpirvPasses/FixIterationRPBarrierPass.h"
@@ -1985,6 +1986,17 @@ namespace MobileGL {
return RunOptimizerChecked("ZeroBaseVertexForVulkan", optimizer, inputBinary, outputBinary, true, enableSpirvValidation);
}
bool ShaderCompiler::LowerPixelCenterIntegerForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
using namespace spvtools;
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(LowerPixelCenterIntegerPass::CreateLowerPixelCenterIntegerPass());
return RunOptimizerChecked("LowerPixelCenterIntegerForVulkan", optimizer, inputBinary, outputBinary,
true, enableSpirvValidation);
}
bool ShaderCompiler::DeriveNumSubgroupsForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
@@ -347,6 +347,12 @@ namespace MobileGL {
static bool ZeroBaseVertexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Drops `layout(pixel_center_integer)` from a fragment module, which the Vulkan
// environment forbids, and shifts every gl_FragCoord.xy read by -0.5 to keep its
// values. See LowerPixelCenterIntegerPass.
static bool LowerPixelCenterIntegerForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
bool enableSpirvValidation = false);
// Replaces compute gl_NumSubgroups loads with ceil(workgroup invocations /
// gl_SubgroupSize). DirectVulkan only; this repairs drivers whose builtin
// disagrees with the subgroup IDs the same dispatch emits (Adreno reports 1
@@ -0,0 +1,188 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.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 "LowerPixelCenterIntegerPass.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/util/make_unique.h"
#include <bit>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
// OpExecutionMode <entry-point> <mode> [literals...]
constexpr uint32_t kExecutionModeModeOperand = 1;
// Returns the Input OpVariable decorated BuiltIn FragCoord, or nullptr if none.
Instruction* FindFragCoordVariable(IRContext* context) {
auto* defUseMgr = context->get_def_use_mgr();
for (auto& annotation : context->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate || annotation.NumInOperands() < 3) continue;
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn)
continue;
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) != spv::BuiltIn::FragCoord)
continue;
Instruction* variable = defUseMgr->GetDef(annotation.GetSingleWordInOperand(0));
if (variable != nullptr && variable->opcode() == spv::Op::OpVariable &&
static_cast<spv::StorageClass>(variable->GetSingleWordInOperand(0)) ==
spv::StorageClass::Input) {
return variable;
}
}
return nullptr;
}
Bool IsDeclarationUse(const Instruction* user) {
switch (user->opcode()) {
case spv::Op::OpDecorate:
case spv::Op::OpDecorateId:
case spv::Op::OpDecorateString:
case spv::Op::OpName:
case spv::Op::OpEntryPoint:
return true;
default:
return false;
}
}
// One gl_FragCoord read to shift: the load and the id of the constant to subtract.
struct ShiftedLoad {
Instruction* load = nullptr;
uint32_t offsetId = 0;
};
} // namespace
spvtools::opt::Pass::Status LowerPixelCenterIntegerPass::Process() {
auto* irContext = context();
auto* defUseMgr = irContext->get_def_use_mgr();
auto* constantMgr = irContext->get_constant_mgr();
auto* typeMgr = irContext->get_type_mgr();
std::vector<Instruction*> modes;
for (Instruction& mode : irContext->module()->execution_modes()) {
if (mode.opcode() == spv::Op::OpExecutionMode &&
mode.NumInOperands() > kExecutionModeModeOperand &&
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(kExecutionModeModeOperand)) ==
spv::ExecutionMode::PixelCenterInteger) {
modes.push_back(&mode);
}
}
if (modes.empty()) return Status::SuccessWithoutChange;
// Gather every read and its offset constant before mutating anything, so a shape
// the pass refuses leaves the module exactly as it was.
std::vector<ShiftedLoad> loads;
Bool refused = false;
const auto scalarOffset = [&](uint32_t floatTypeId, float value) -> uint32_t {
const auto* floatType = typeMgr->GetType(floatTypeId);
if (floatType == nullptr || floatType->AsFloat() == nullptr) return 0;
const auto* constant = constantMgr->GetConstant(floatType, {std::bit_cast<uint32_t>(value)});
const Instruction* inst = constant ? constantMgr->GetDefiningInstruction(constant, floatTypeId) : nullptr;
return inst ? inst->result_id() : 0;
};
if (Instruction* fragCoord = FindFragCoordVariable(irContext)) {
const uint32_t fragCoordId = fragCoord->result_id();
defUseMgr->ForEachUser(fragCoord, [&](Instruction* user) {
if (refused || IsDeclarationUse(user)) return;
if (user->opcode() == spv::Op::OpLoad && user->GetSingleWordInOperand(0) == fragCoordId) {
// The whole vec4: subtract (0.5, 0.5, 0, 0).
const auto* vecType = typeMgr->GetType(user->type_id());
const auto* vector = vecType ? vecType->AsVector() : nullptr;
if (vector == nullptr || vector->element_count() != 4) {
refused = true;
return;
}
const uint32_t floatTypeId = typeMgr->GetId(vector->element_type());
const uint32_t half = scalarOffset(floatTypeId, 0.5f);
const uint32_t zero = scalarOffset(floatTypeId, 0.0f);
const auto* composite =
(half && zero) ? constantMgr->GetConstant(vecType, {half, half, zero, zero}) : nullptr;
const Instruction* inst =
composite ? constantMgr->GetDefiningInstruction(composite, user->type_id()) : nullptr;
if (inst == nullptr) {
refused = true;
return;
}
loads.push_back({user, inst->result_id()});
return;
}
if ((user->opcode() == spv::Op::OpAccessChain ||
user->opcode() == spv::Op::OpInBoundsAccessChain) &&
user->NumInOperands() == 2 && user->GetSingleWordInOperand(0) == fragCoordId) {
const auto* index = constantMgr->FindDeclaredConstant(user->GetSingleWordInOperand(1));
if (index == nullptr || index->AsScalarConstant() == nullptr) {
refused = true;
return;
}
const Uint64 component = index->GetZeroExtendedValue();
defUseMgr->ForEachUser(user, [&](Instruction* chainUser) {
if (refused || IsDeclarationUse(chainUser)) return;
if (chainUser->opcode() != spv::Op::OpLoad) {
refused = true;
return;
}
// z and w keep their values; only the centre of x and y moves.
if (component >= 2) return;
const uint32_t half = scalarOffset(chainUser->type_id(), 0.5f);
if (half == 0) {
refused = true;
return;
}
loads.push_back({chainUser, half});
});
return;
}
refused = true;
});
}
if (refused) return Status::Failure;
for (Instruction* mode : modes) irContext->KillInst(mode);
// `%r = OpLoad %ty %ptr` becomes `%fresh = OpLoad %ty %ptr` followed by
// `%r = OpFSub %ty %fresh %offset`: every existing use of %r picks the shifted
// value up without being rewritten.
for (const ShiftedLoad& shifted : loads) {
const uint32_t freshId = TakeNextId();
if (freshId == 0) return Status::Failure;
const uint32_t resultId = shifted.load->result_id();
const uint32_t typeId = shifted.load->type_id();
// A load is never the last instruction of its block (a terminator follows),
// so inserting before its successor places the subtraction right after it.
Instruction* next = shifted.load->NextNode();
if (next == nullptr) return Status::Failure;
shifted.load->SetResultId(freshId);
next->InsertBefore(MakeUnique<Instruction>(
irContext, spv::Op::OpFSub, typeId, resultId,
Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {freshId}},
{SPV_OPERAND_TYPE_ID, {shifted.offsetId}}}));
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken LowerPixelCenterIntegerPass::CreateLowerPixelCenterIntegerPass() {
return spvtools::Optimizer::PassToken(MakeUnique<LowerPixelCenterIntegerPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,38 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.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 {
// Lowers `layout(pixel_center_integer) in vec4 gl_FragCoord;` for the Vulkan
// environment, which forbids the PixelCenterInteger execution mode
// (VUID-StandaloneSpirv-PixelCenterInteger-04654) - a module carrying it fails
// validation and DirectVulkan refuses every draw of the program. The mode only moves
// the pixel centre from (n + 0.5) to n, so the pass drops it and subtracts 0.5 from
// x and y of every gl_FragCoord read: whole-vector loads get vec4(0.5, 0.5, 0, 0),
// component loads through a constant-index access chain get 0.5 for x and y.
//
// Any other use of the builtin (a non-constant component index, the pointer handed
// to a function) fails the pass rather than half-rewriting it; the caller then keeps
// the original module, which is no worse than before.
class LowerPixelCenterIntegerPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "lower-pixel-center-integer"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateLowerPixelCenterIntegerPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL