[Fix] (Magma): ignore shader gl_SampleMask without multisample fragment ops

This commit is contained in:
2026-10-12 19:37:25 -04:00
parent 05d58a566a
commit 065d1582ba
13 changed files with 402 additions and 5 deletions
+1
View File
@@ -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
@@ -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<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
stagesWithPassthrough = *payload.stages;
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
effectiveStages = &stagesWithPassthrough;
Vector<VkPipelineShaderStageCreateInfo> 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;
@@ -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;
@@ -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<Uint> 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
@@ -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<VkPipelineShaderStageCreateInfo> 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<VkShaderModule> 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
}
};
@@ -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<Uint32>(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;
@@ -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;
@@ -22,6 +22,7 @@ add_executable(
FragCoordYFlipTest.cpp
PixelCenterIntegerTest.cpp
ZeroLayerOutputTest.cpp
FullCoverageSampleMaskTest.cpp
RelaxedOpaqueStructTest.cpp
ShadingRateEsslTest.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 <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 {
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<Uint32> CompileFragment(const char* fragment) {
using namespace MG_Util::ShaderTranspiler;
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;
}
// 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<Uint32> 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<Uint32> 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<Uint32> 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<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FullCoverageSampleMaskForVulkan(input, output, true));
EXPECT_EQ(output, input);
}
@@ -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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary,
const bool enableSpirvValidation) {
@@ -359,6 +359,13 @@ namespace MobileGL {
static bool ZeroLayerOutputForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& 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<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,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 <vector>
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<spv::Decoration>(annotation.GetSingleWordInOperand(1)) !=
spv::Decoration::BuiltIn)
continue;
if (static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(2)) !=
spv::BuiltIn::SampleMask)
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::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<Instruction*> arrayStores;
std::vector<Instruction*> 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<uint32_t> 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<FullCoverageSampleMaskPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -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 <Includes.h>
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