From da6b172d0821502c84eb175a85f9e1be55f6ddeb Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 15:06:42 -0400 Subject: [PATCH] [Fix] (Magma): lower pixel_center_integer gl_FragCoord so Vulkan validation stops refusing the program --- CMakeLists.txt | 1 + .../DirectVulkan/Renderer/ProgramFactory.cpp | 14 ++ .../MG_Test/ShaderTranspiler/CMakeLists.txt | 1 + .../PixelCenterIntegerTest.cpp | 131 ++++++++++++ .../ShaderTranspiler/ShaderCompiler.cpp | 12 ++ .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 6 + .../LowerPixelCenterIntegerPass.cpp | 188 ++++++++++++++++++ .../SpirvPasses/LowerPixelCenterIntegerPass.h | 38 ++++ 8 files changed, 391 insertions(+) create mode 100644 MobileGL/MG_Test/ShaderTranspiler/PixelCenterIntegerTest.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0955eae9..d5bc10b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index e0727381..79e0de75 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.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 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 fragCoordSpirv; if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) { diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index c2768ce3..ee131a5b 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable( GlslangCaptureTest.cpp DemotePointSizeTest.cpp FragCoordYFlipTest.cpp + PixelCenterIntegerTest.cpp RelaxedOpaqueStructTest.cpp ShadingRateEsslTest.cpp ) diff --git a/MobileGL/MG_Test/ShaderTranspiler/PixelCenterIntegerTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/PixelCenterIntegerTest.cpp new file mode 100644 index 00000000..c778895a --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/PixelCenterIntegerTest.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 + +#include "Includes.h" +#include "Init.h" +#include +#include + +#include "spirv-tools/libspirv.hpp" + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; + +namespace { + Vector 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> shaders; + for (const auto& [stage, source] : Vector>{{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 module = binary.value()[1]; + EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true)); + return module; + } + + String Disassemble(const Vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + String text; + EXPECT_TRUE(tools.Disassemble(spirv, &text)); + return text; + } + + String ValidationError(const Vector& 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 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 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 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 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 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 output; + ASSERT_TRUE(ShaderCompiler::LowerPixelCenterIntegerForVulkan(input, output, true)); + EXPECT_EQ(Disassemble(output), Disassemble(input)); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index ae2579f9..e2bcf31b 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -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& inputBinary, + Vector& 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& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 3ff5de0e..0f57450c 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -347,6 +347,12 @@ namespace MobileGL { static bool ZeroBaseVertexForVulkan(const Vector& inputBinary, Vector& 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& inputBinary, + Vector& 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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp new file mode 100644 index 00000000..e778e3a1 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp @@ -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 +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + + // OpExecutionMode [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(annotation.GetSingleWordInOperand(1)) != + spv::Decoration::BuiltIn) + continue; + if (static_cast(annotation.GetSingleWordInOperand(2)) != spv::BuiltIn::FragCoord) + continue; + Instruction* variable = defUseMgr->GetDef(annotation.GetSingleWordInOperand(0)); + if (variable != nullptr && variable->opcode() == spv::Op::OpVariable && + static_cast(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 modes; + for (Instruction& mode : irContext->module()->execution_modes()) { + if (mode.opcode() == spv::Op::OpExecutionMode && + mode.NumInOperands() > kExecutionModeModeOperand && + static_cast(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 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(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( + 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()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.h new file mode 100644 index 00000000..12f6de1c --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.h @@ -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 + +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