diff --git a/CMakeLists.txt b/CMakeLists.txt index 77eefeb1..25fb6ffe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -418,6 +418,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerPixelCenterIntegerPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroLayerOutputPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.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/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index aff62450..5d27f1a3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -223,6 +223,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { sizeof(payload.passthroughTessControlKey))); XXHASH_VERIFY(XXH64_update(hashState, &payload.viewportCount, sizeof(payload.viewportCount))); XXHASH_VERIFY(XXH64_update(hashState, &payload.nonLayeredStages, sizeof(payload.nonLayeredStages))); + const Bool fullCoverageSampleMask = payload.fullCoverageFragmentModule != VK_NULL_HANDLE; + XXHASH_VERIFY(XXH64_update(hashState, &fullCoverageSampleMask, sizeof(fullCoverageSampleMask))); XXHASH_VERIFY(XXH64_update(hashState, &payload.polygonMode, sizeof(payload.polygonMode))); XXHASH_VERIFY(XXH64_update(hashState, &payload.cullMode, sizeof(payload.cullMode))); XXHASH_VERIFY(XXH64_update(hashState, &payload.frontFace, sizeof(payload.frontFace))); @@ -507,11 +509,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { // wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection // takes: the draw is skipped, nothing is memoised, and the process survives. const Vector* effectiveStages = payload.stages; - Vector stagesWithPassthrough; - if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) { - stagesWithPassthrough = *payload.stages; - stagesWithPassthrough.push_back(payload.passthroughTessControlStage); - effectiveStages = &stagesWithPassthrough; + Vector adjustedStages; + if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE || + payload.fullCoverageFragmentModule != VK_NULL_HANDLE) { + adjustedStages = *payload.stages; + if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) { + adjustedStages.push_back(payload.passthroughTessControlStage); + } + if (payload.fullCoverageFragmentModule != VK_NULL_HANDLE) { + for (auto& stageInfo : adjustedStages) { + if (stageInfo.stage == VK_SHADER_STAGE_FRAGMENT_BIT) { + stageInfo.module = payload.fullCoverageFragmentModule; + } + } + } + effectiveStages = &adjustedStages; } { VkShaderStageFlags stagesPresent = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index b61b82b6..269f480f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -85,6 +85,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // a non-layered framebuffer uses (ZeroLayerOutputPass). Hashed - the two stage sets // are different pipelines; the program hash alone names both. Bool nonLayeredStages = false; + // Non-null: the fragment stage in `stages` is replaced by this module, the program's + // full-coverage gl_SampleMask variant (VkProgramObject::fullCoverageFragmentModule), + // because this draw has no multisample fragment operations. Hashed as presence only - + // the module is a pure function of the program, which programHash already names. + VkShaderModule fullCoverageFragmentModule = VK_NULL_HANDLE; VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL; VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT; VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 31f002fd..873ece7d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -4024,6 +4024,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } if (!entry.nonLayeredStages.empty()) entry.nonLayeredStages.push_back(nonLayeredStage); + + // GL ignores gl_SampleMask without multisample fragment operations; Vulkan discards + // a fragment whose one-sample mask has bit 0 clear. A fragment stage that stores it + // gets a second module storing all-ones for those draws (FullCoverageSampleMaskPass). + if (shaderStage == ShaderStage::Fragment) { + Vector fullCoverageSpv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::FullCoverageSampleMaskForVulkan( + moduleSpv, fullCoverageSpv, enableSpirvValidation) && + !fullCoverageSpv.empty() && fullCoverageSpv != moduleSpv) { + VkShaderModuleCreateInfo fullCoverageInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + fullCoverageInfo.codeSize = fullCoverageSpv.size() * sizeof(Uint); + fullCoverageInfo.pCode = fullCoverageSpv.data(); + VkShaderModule fullCoverageModule = VK_NULL_HANDLE; + VK_VERIFY(vkCreateShaderModule(m_device, &fullCoverageInfo, nullptr, &fullCoverageModule), + "vkCreateShaderModule(full-coverage gl_SampleMask)"); + entry.modules.push_back(fullCoverageModule); + entry.fullCoverageFragmentModule = fullCoverageModule; + } + } } // Reflect and create layout as part of the program object diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 55de3d6d..b5986403 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -134,6 +134,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { // program that never writes gl_Layer, which then uses `stages` for both. The variant // modules are owned by `modules` like the others. Vector nonLayeredStages; + // The fragment module a draw without multisample fragment operations uses (a + // single-sample framebuffer, or MULTISAMPLE disabled): every gl_SampleMask store + // writes all-ones, since GL ignores the shader's mask there and Vulkan would discard + // on a clear bit 0 (FullCoverageSampleMaskPass). VK_NULL_HANDLE for every program + // whose fragment stage never writes gl_SampleMask. Owned by `modules`. + VkShaderModule fullCoverageFragmentModule = VK_NULL_HANDLE; Vector modules; // Parallel to stages; identifies the exact module bytes handed to the driver when a // pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V. @@ -284,6 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { hash = other.hash; stages = std::move(other.stages); nonLayeredStages = std::move(other.nonLayeredStages); + fullCoverageFragmentModule = other.fullCoverageFragmentModule; + other.fullCoverageFragmentModule = VK_NULL_HANDLE; modules = std::move(other.modules); // Must travel with `modules`: these digests name the SPIR-V those exact // shader modules were built from, and the pipeline-failure diagnostics @@ -364,6 +372,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { hash = other.hash; stages = std::move(other.stages); nonLayeredStages = std::move(other.nonLayeredStages); + fullCoverageFragmentModule = other.fullCoverageFragmentModule; + other.fullCoverageFragmentModule = VK_NULL_HANDLE; modules = std::move(other.modules); stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor descriptorSetLayout = other.descriptorSetLayout; @@ -455,6 +465,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { modules.clear(); stages.clear(); nonLayeredStages.clear(); + fullCoverageFragmentModule = VK_NULL_HANDLE; stageSpirvDigests.clear(); // the modules they describe are gone } }; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 4bfda4f6..eb2a4abd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2825,6 +2825,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool primitiveRestartEnable, Bool layeredFramebuffer, Uint64 wireRenderPassCompatibilityId) { Bool invertClockwise = transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip; const Bool nonLayeredStages = !layeredFramebuffer && !programObj.nonLayeredStages.empty(); + // GL 4.6 core 17.3.3: the shader's gl_SampleMask is a multisample fragment operation, a + // no-op on a single-sample framebuffer or with MULTISAMPLE disabled - the same gate + // ResolveEffectiveSampleMask applies to the fixed-function mask. + const Bool fullCoverageSampleMask = + programObj.fullCoverageFragmentModule != VK_NULL_HANDLE && + (renderPassEntry.sampleCount == VK_SAMPLE_COUNT_1_BIT || + !MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::Multisample)); if (programObj.stages.empty()) { MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages"); return VK_NULL_HANDLE; @@ -2876,6 +2883,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.renderStateCso == renderStateCso && entry.primitiveRestartEnable == primitiveRestartEnable && entry.nonLayeredStages == nonLayeredStages && + entry.fullCoverageSampleMask == fullCoverageSampleMask && entry.transformFlags == transformFlags) { if (MG_Util::PipeStats::Enabled()) { // Gate 5 of section 2.3.1. On a hit this whole function cost the one @@ -3164,6 +3172,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { .patchControlPoints = static_cast(MG_Pipe::gPipeInputs->GetPatchVertices()), .viewportCount = ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin), .nonLayeredStages = nonLayeredStages, + .fullCoverageFragmentModule = + fullCoverageSampleMask ? programObj.fullCoverageFragmentModule : VK_NULL_HANDLE, .polygonMode = effectivePolygonMode, .cullMode = cullFaceEnabled ? MG_Util::ConvertCullFaceModeToVkEnum(MG_Pipe::gPipeInputs->GetCullFaceMode(), false) @@ -3449,6 +3459,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.renderStateCso = renderStateCso; entry.primitiveRestartEnable = primitiveRestartEnable; entry.nonLayeredStages = nonLayeredStages; + entry.fullCoverageSampleMask = fullCoverageSampleMask; entry.transformFlags = transformFlags; entry.pipeline = pipeline; m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index d7c962fa..c52f4632 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -1472,6 +1472,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // PipelineCreatePayload::nonLayeredStages). Not derivable from the render pass: a // layered and a single-layer attachment of one format share it. Bool nonLayeredStages = false; + // The pipeline was built with the program's full-coverage gl_SampleMask fragment + // module (PipelineCreatePayload::fullCoverageFragmentModule). Follows from the + // render pass's sample count and MULTISAMPLE, kept here so the key states it. + Bool fullCoverageSampleMask = false; VkPipeline pipeline = VK_NULL_HANDLE; }; static constexpr Uint32 kPipelineMemoSize = 8; diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index ca3498db..b6a08124 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -22,6 +22,7 @@ add_executable( FragCoordYFlipTest.cpp PixelCenterIntegerTest.cpp ZeroLayerOutputTest.cpp + FullCoverageSampleMaskTest.cpp RelaxedOpaqueStructTest.cpp ShadingRateEsslTest.cpp ) diff --git a/MobileGL/MG_Test/ShaderTranspiler/FullCoverageSampleMaskTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/FullCoverageSampleMaskTest.cpp new file mode 100644 index 00000000..b4a16d97 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/FullCoverageSampleMaskTest.cpp @@ -0,0 +1,124 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FullCoverageSampleMaskTest.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 + +// GL ignores a fragment shader's gl_SampleMask without multisample fragment operations; Vulkan +// discards on a clear bit 0 at one sample (KHR-GL4x.sample_variables.mask.*.samples_0 drew +// nothing). The single-sample variant must validate and store nothing but all-ones. + +#include + +#include "Includes.h" +#include "Init.h" +#include +#include + +#include "spirv-tools/libspirv.hpp" + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; + +namespace { + constexpr const char* kVertex = R"(#version 450 core +layout(location = 0) in vec2 position; +void main() { gl_Position = vec4(position, 0.0, 1.0); } +)"; + + // The fragment module of a vertex + fragment program (stages come back sorted). + Vector CompileFragment(const char* fragment) { + using namespace MG_Util::ShaderTranspiler; + 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; + } + + // The CTS shape: the mask is computed from a uniform and gl_SampleMaskIn, one store per word. + constexpr const char* kMaskingFragment = R"(#version 450 core +layout(location = 0) out vec4 color; +uniform int u_sampleMask; +void main() { + gl_SampleMask[0] = u_sampleMask & gl_SampleMaskIn[0]; + color = vec4(1.0, 0.0, 0.0, 1.0); +} +)"; +} // namespace + +TEST(FullCoverageSampleMask, EverySampleMaskStoreWritesAllOnes) { + MobileGL::Initialize(); + const Vector input = CompileFragment(kMaskingFragment); + ASSERT_FALSE(input.empty()); + const String original = Disassemble(input); + ASSERT_NE(original.find("BuiltIn SampleMask"), String::npos) << original; + ASSERT_EQ(CountOccurrences(original, "%int_n1"), 0u) << original; + + Vector output; + ASSERT_TRUE(ShaderCompiler::FullCoverageSampleMaskForVulkan(input, output, true)); + EXPECT_EQ(ValidationError(output), String{}); + EXPECT_NE(output, input); + const String text = Disassemble(output); + // Every store into the builtin now stores the all-ones constant. + SizeT stores = 0; + for (SizeT at = text.find("OpStore "); at != String::npos; at = text.find("OpStore ", at + 1)) { + const SizeT end = text.find('\n', at); + const String line = text.substr(at, end - at); + if (line.find("%int_n1") != String::npos) ++stores; + } + EXPECT_GE(stores, 1u) << text; + EXPECT_NE(text.find("%int_n1 = OpConstant %int -1"), String::npos) << text; +} + +// A module that never writes gl_SampleMask is left alone, so its program keeps one module. +TEST(FullCoverageSampleMask, LeavesModulesWithoutSampleMaskUnchanged) { + MobileGL::Initialize(); + const Vector input = CompileFragment(R"(#version 450 core +layout(location = 0) out vec4 color; +void main() { color = vec4(float(gl_SampleMaskIn[0] & 1)); } +)"); + ASSERT_FALSE(input.empty()); + Vector output; + ASSERT_TRUE(ShaderCompiler::FullCoverageSampleMaskForVulkan(input, output, true)); + EXPECT_EQ(output, input); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 6ebb5b49..b4ae0ef7 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -35,6 +35,7 @@ #include "SpirvPasses/ZeroBaseVertexPass.h" #include "SpirvPasses/LowerPixelCenterIntegerPass.h" #include "SpirvPasses/ZeroLayerOutputPass.h" +#include "SpirvPasses/FullCoverageSampleMaskPass.h" #include "SpirvPasses/DeriveNumSubgroupsPass.h" #include "SpirvPasses/EmulateSubgroupsPass.h" #include "SpirvPasses/FixIterationRPBarrierPass.h" @@ -2009,6 +2010,17 @@ namespace MobileGL { enableSpirvValidation); } + bool ShaderCompiler::FullCoverageSampleMaskForVulkan(const Vector& inputBinary, + Vector& outputBinary, + const bool enableSpirvValidation) { + using namespace spvtools; + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(FullCoverageSampleMaskPass::CreateFullCoverageSampleMaskPass()); + + return RunOptimizerChecked("FullCoverageSampleMaskForVulkan", 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 70c66bb7..b239c365 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -359,6 +359,13 @@ namespace MobileGL { static bool ZeroLayerOutputForVulkan(const Vector& inputBinary, Vector& outputBinary, bool enableSpirvValidation = false); + // Builds the variant of a fragment module that a draw without multisample fragment + // operations (single-sample framebuffer, or MULTISAMPLE disabled) uses: every + // gl_SampleMask store writes all-ones, since GL ignores the mask there. A module + // that writes no SampleMask comes back unchanged. See FullCoverageSampleMaskPass. + static bool FullCoverageSampleMaskForVulkan(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/FullCoverageSampleMaskPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.cpp new file mode 100644 index 00000000..64cd9c94 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.cpp @@ -0,0 +1,148 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.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 "FullCoverageSampleMaskPass.h" + +#include "spirv.hpp" +#include "source/opt/constants.h" +#include "source/opt/def_use_manager.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_context.h" +#include "source/opt/module.h" +#include "source/opt/type_manager.h" +#include "source/util/make_unique.h" + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + + // Returns the Output OpVariable decorated BuiltIn SampleMask, or nullptr if none. + Instruction* FindSampleMaskOutputVariable(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::SampleMask) + continue; + Instruction* variable = defUseMgr->GetDef(annotation.GetSingleWordInOperand(0)); + if (variable != nullptr && variable->opcode() == spv::Op::OpVariable && + static_cast(variable->GetSingleWordInOperand(0)) == + spv::StorageClass::Output) { + 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; + } + } + } // namespace + + spvtools::opt::Pass::Status FullCoverageSampleMaskPass::Process() { + auto* irContext = context(); + auto* defUseMgr = irContext->get_def_use_mgr(); + auto* constantMgr = irContext->get_constant_mgr(); + auto* typeMgr = irContext->get_type_mgr(); + + Instruction* sampleMask = FindSampleMaskOutputVariable(irContext); + if (sampleMask == nullptr) return Status::SuccessWithoutChange; + + // Gather the stores before mutating anything, so a shape the pass refuses leaves + // the module exactly as it was. Whole-array stores and element stores are kept + // apart: they need an all-ones array and an all-ones element respectively. + const uint32_t sampleMaskId = sampleMask->result_id(); + std::vector arrayStores; + std::vector elementStores; + Bool refused = false; + defUseMgr->ForEachUser(sampleMask, [&](Instruction* user) { + if (refused || IsDeclarationUse(user)) return; + if (user->opcode() == spv::Op::OpStore && user->GetSingleWordInOperand(0) == sampleMaskId) { + arrayStores.push_back(user); + return; + } + if ((user->opcode() == spv::Op::OpAccessChain || + user->opcode() == spv::Op::OpInBoundsAccessChain) && + user->NumInOperands() == 2) { + const uint32_t chainId = user->result_id(); + defUseMgr->ForEachUser(user, [&](Instruction* chainUser) { + if (refused || IsDeclarationUse(chainUser)) return; + if (chainUser->opcode() == spv::Op::OpStore && + chainUser->GetSingleWordInOperand(0) == chainId) { + elementStores.push_back(chainUser); + return; + } + refused = true; + }); + return; + } + refused = true; + }); + if (refused) return Status::Failure; + if (arrayStores.empty() && elementStores.empty()) return Status::SuccessWithoutChange; + + const auto* pointerType = typeMgr->GetType(sampleMask->type_id()); + const auto* pointer = pointerType ? pointerType->AsPointer() : nullptr; + const auto* arrayType = pointer ? pointer->pointee_type() : nullptr; + const auto* array = arrayType ? arrayType->AsArray() : nullptr; + const auto* elementType = array ? array->element_type() : nullptr; + const auto* integer = elementType ? elementType->AsInteger() : nullptr; + if (integer == nullptr || integer->width() != 32) return Status::Failure; + + const uint32_t elementTypeId = typeMgr->GetId(elementType); + const auto* allOnes = constantMgr->GetConstant(elementType, {0xffffffffu}); + const Instruction* allOnesInst = + allOnes ? constantMgr->GetDefiningInstruction(allOnes, elementTypeId) : nullptr; + if (elementTypeId == 0 || allOnesInst == nullptr) return Status::Failure; + for (Instruction* store : elementStores) store->SetInOperand(1, {allOnesInst->result_id()}); + + if (!arrayStores.empty()) { + // The array's length is a constant (the builtin is sized at link time); a + // specialization-constant length is not something this pass can fill. + const auto& lengthInfo = array->length_info(); + if (lengthInfo.words.size() != 2 || + lengthInfo.words[0] != spvtools::opt::analysis::Array::LengthInfo::kConstant) { + return Status::Failure; + } + const uint32_t length = lengthInfo.words[1]; + const uint32_t arrayTypeId = typeMgr->GetId(arrayType); + std::vector elementIds(length, allOnesInst->result_id()); + const auto* allOnesArray = constantMgr->GetConstant(arrayType, elementIds); + const Instruction* allOnesArrayInst = + allOnesArray ? constantMgr->GetDefiningInstruction(allOnesArray, arrayTypeId) : nullptr; + if (arrayTypeId == 0 || allOnesArrayInst == nullptr) return Status::Failure; + for (Instruction* store : arrayStores) store->SetInOperand(1, {allOnesArrayInst->result_id()}); + } + + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken FullCoverageSampleMaskPass::CreateFullCoverageSampleMaskPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.h new file mode 100644 index 00000000..3d4b2fcf --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.h @@ -0,0 +1,42 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FullCoverageSampleMaskPass.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 { + // GL applies a fragment shader's gl_SampleMask only among the multisample fragment + // operations, which make no change "if MULTISAMPLE is disabled, or if the value of + // SAMPLE_BUFFERS is not one" (GL 4.6 core 17.3.3). Vulkan ANDs the SampleMask output + // with coverage at every rasterizationSamples, so at one sample a mask with bit 0 + // clear discards the fragment (KHR-GL4x.sample_variables.mask.*.samples_0 drew nothing + // into a single-sample texture). This pass builds the fragment module such a draw + // uses: every store to the SampleMask output stores all-ones, which leaves coverage + // as the rasterizer produced it. + // + // Kept as a second module beside the original, like ZeroLayerOutputPass, so the + // multisample draws of the same program keep the shader's mask. + // + // Handles whole-array stores and stores through an access chain into the array. Any + // other use of the builtin (a read-back of the output, a chain used for anything but + // a store) fails the pass, and the caller then keeps the original module. + class FullCoverageSampleMaskPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "full-coverage-sample-mask"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateFullCoverageSampleMaskPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL