mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[Fix] (Magma): ignore shader gl_SampleMask without multisample fragment ops
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user