Compare commits

...
3 Commits
17 changed files with 1081 additions and 139 deletions
+9
View File
@@ -455,6 +455,15 @@ jobs:
if [ '${{ matrix.backend }}' = 'DirectVulkan' ]; then
export MOBILEGL_MAGMA_R11G11B10F_FALLBACK=1
fi
# The blended depth-write quirk auto-enables only on Qualcomm, which no CI
# runner has, so force it on for the OIT case it exists to fix. ForceOn
# bypasses only the vendor gate, so this exercises the real strip on
# lavapipe. The Android AVD lane deliberately leaves it off, keeping the
# unstripped path covered for the same trace.
if [ '${{ matrix.backend }}' = 'DirectVulkan' ] \
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
fi
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
- name: Upload actual image
+1
View File
@@ -188,6 +188,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
+5 -3
View File
@@ -81,9 +81,11 @@ namespace MobileGL::MG_Config {
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from blended pipelines on drivers without cross-pipeline
// vertex position invariance (see VulkanRenderer's PipelineFactory setup). Auto
// detects Qualcomm.
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
// cross-pipeline vertex position invariance. Sorted-transparency "over" blends,
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// feature off. It is enabled by default to match GL's defined out-of-range fetch
@@ -109,10 +109,81 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"vkCreatePipelineCache");
}
// Must be called once, before any pipeline is created: the flag is not part of the
// pipeline hash, so flipping it mid-life would serve cached pipelines built under the
// old value.
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
s_suppressBlendedDepthWrite = enabled;
}
Bool PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId) {
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
switch (quirkOverride) {
case MG_Config::QuirkOverride::ForceOn:
return true;
case MG_Config::QuirkOverride::ForceOff:
return false;
case MG_Config::QuirkOverride::Auto:
default:
return vendorId == kVendorIdQualcomm;
}
}
namespace {
// Order-independent accumulation blending: the write order of overlapping fragments
// does not change the result, which is what lets multi-pass chains re-rasterize the
// same geometry and combine per-pass contributions (MC 26.3 OIT: GL_MAX depth
// bounds, additive ONE+ONE transmittance/accumulate). Sorted-transparency "over"
// compositing (SRC_ALPHA-style factors) is order-dependent, drawn once per surface,
// and relies on its depth writes for occlusion - it must not be treated as hazardous.
// MIN/MAX ignore blend factors entirely per the Vulkan spec.
//
// Deliberately color-channel only. A separate-alpha accumulation
// (glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX)) whose color channel is an ordinary
// over-blend is not treated as hazardous: no known content pairs that shape with a
// depth-equality chain, and widening the test would re-capture sorted transparency.
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
if (attachment.colorBlendOp == VK_BLEND_OP_MIN || attachment.colorBlendOp == VK_BLEND_OP_MAX) {
return true;
}
return attachment.colorBlendOp == VK_BLEND_OP_ADD &&
attachment.srcColorBlendFactor == VK_BLEND_FACTOR_ONE &&
attachment.dstColorBlendFactor == VK_BLEND_FACTOR_ONE;
}
} // namespace
Bool PipelineFactory::ShouldSuppressDepthWrite(const PipelineCreatePayload& payload) {
if (!payload.depthWriteEnable) {
return false;
}
// A shader that assigns gl_FragDepth supplies depth itself rather than taking the
// pipeline's interpolated Z, so a driver that varies the vertex position math
// between pipelines cannot desynchronize it. (A gl_FragDepth = gl_FragCoord.z
// passthrough is the exception that stays exposed; no known content pairs one with
// an equality chain, and 26.3's composite is a genuine computed-depth writer.)
if (payload.fragmentReplacesDepth) {
return false;
}
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const VkPipelineColorBlendAttachmentState& attachment = payload.colorBlendAttachments[i];
if (attachment.blendEnable != VK_TRUE) {
continue;
}
// All color writes masked: blending is moot (depth-prepass pattern that left
// GL_BLEND enabled); stripping the depth write would delete the whole prepass.
if (attachment.colorWriteMask == 0) {
continue;
}
// Any attachment qualifies, not just attachment 0: the 26.3 transmittance pass
// accumulates into a 2-target MRT and must stay stripped.
if (IsAccumulationBlend(attachment)) {
return true;
}
}
return false;
}
PipelineFactory::~PipelineFactory() {
DestroyAll();
if (m_pipelineCache != VK_NULL_HANDLE) {
@@ -157,6 +228,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
if (payload.colorAttachmentCount > 0) {
XXHASH_VERIFY(XXH64_update(
m_hashState,
@@ -263,17 +336,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
colorAttachments[i] = payload.colorBlendAttachments[i];
}
// Suppress depth writes on blended pipelines when the active driver cannot keep
// vertex positions invariant across the pipelines of a multi-pass depth-equality
// chain (see SetSuppressBlendedDepthWrite). Blended draws that write depth are rare
// and the equality-dependent prepass pattern is exactly the case that breaks.
if (s_suppressBlendedDepthWrite && depthStencil.depthWriteEnable == VK_TRUE) {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
if (colorAttachments[i].blendEnable == VK_TRUE) {
depthStencil.depthWriteEnable = VK_FALSE;
break;
}
}
// Suppress depth writes on accumulation-blended pipelines when the active driver
// cannot keep vertex positions invariant across the pipelines of a multi-pass
// depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed
// in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water),
// gl_FragDepth writers, and masked-out attachments keep their depth writes.
// This bakes the decision into the pipeline, which only works because depth write is
// static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates
// would let the record-time value override it and silently disable the quirk.
if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) {
depthStencil.depthWriteEnable = VK_FALSE;
}
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
@@ -49,6 +49,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS;
// The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the
// pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
@@ -62,13 +65,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
void DestroyAll();
// Driver quirk: suppress depth writes on blended pipelines. Multi-pass depth-equality
// rendering (a blended prepass writes depth that later passes re-test with an
// equality-inclusive compare on the re-rasterized geometry) requires cross-pipeline
// position invariance that some mobile compilers do not provide, even with the
// SPIR-V Invariant decoration; whole primitives then drop out of the later passes.
// Set at renderer initialization based on the active driver.
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
// depth-equality rendering (a blended prepass writes depth that later passes re-test
// with an equality-inclusive compare on the re-rasterized geometry) requires
// cross-pipeline position invariance that some mobile compilers do not provide, even
// with the SPIR-V Invariant decoration; whole primitives then drop out of the later
// passes. Only order-independent accumulation blends (MIN/MAX, additive ONE+ONE) are
// stripped - that is the signature of such equality chains (MC 26.3 OIT) - while
// sorted-transparency "over" compositing (e.g. vanilla MC water, SRC_ALPHA factors),
// which draws each surface once and depends on its depth writes to occlude later
// passes, keeps them. Set at renderer initialization based on the active driver.
static void SetSuppressBlendedDepthWrite(Bool enabled);
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
// the known-affected vendor (Qualcomm).
static Bool ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId);
// Pure per-pipeline strip decision (exempts gl_FragDepth writers, masked-out and
// non-accumulation blends); combined with the device flag in CreatePipeline. Static
// and payload-only so tests can pin the contract without a VkDevice.
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
private:
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
@@ -312,34 +312,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return targetEnv;
}
// Cheap raw-word scan for an `OpDecorate <id> BuiltIn InstanceIndex` decoration. Used
// only to decide whether to warn when shaderDrawParameters is unavailable; a false
// negative merely suppresses a diagnostic.
Bool SpirvDeclaresInstanceIndexBuiltin(const Vector<Uint>& spirv) {
constexpr Uint32 kSpirvMagicNumber = 0x07230203u;
constexpr SizeT kHeaderWordCount = 5;
if (spirv.size() <= kHeaderWordCount || spirv[0] != kSpirvMagicNumber) {
return false;
}
SizeT wordIndex = kHeaderWordCount;
while (wordIndex < spirv.size()) {
const Uint32 firstWord = spirv[wordIndex];
const Uint32 wordCount = firstWord >> 16;
const auto opcode = static_cast<spv::Op>(firstWord & 0xffffu);
if (wordCount == 0 || wordIndex + wordCount > spirv.size()) {
break;
}
if (opcode == spv::Op::OpDecorate && wordCount >= 4 &&
static_cast<spv::Decoration>(spirv[wordIndex + 2]) == spv::Decoration::BuiltIn &&
static_cast<spv::BuiltIn>(spirv[wordIndex + 3]) == spv::BuiltIn::InstanceIndex) {
return true;
}
wordIndex += wordCount;
}
return false;
}
Bool IsInterfaceVariableStaticallyUsed(const Vector<Uint>& spirv, Uint32 spirvId) {
if (spirv.empty() || spirvId == 0) {
return false;
@@ -1218,6 +1190,41 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
} // namespace
// A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself
// instead of taking the pipeline's interpolated Z, so a driver that varies the vertex
// position math between pipelines cannot desynchronize it; the blended depth-write
// quirk therefore leaves it alone (see PipelineFactory::ShouldSuppressDepthWrite).
Bool ProgramFactory::ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule) {
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
for (Uint32 modeIndex = 0; modeIndex < entryPoint.execution_mode_count; ++modeIndex) {
if (entryPoint.execution_modes[modeIndex] == SpvExecutionModeDepthReplacing) {
return true;
}
}
}
return false;
}
// glslang's relaxed-Vulkan mode maps GL's gl_InstanceID onto the InstanceIndex builtin.
// Without shaderDrawParameters there is no gl_BaseInstance to subtract, so such a shader
// cannot be corrected and instanced draws with a non-zero baseInstance misrender; this
// detects the case so the user gets one warning instead of silent corruption.
Bool ProgramFactory::ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule) {
for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) {
const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex];
for (Uint32 variableIndex = 0; variableIndex < entryPoint.input_variable_count; ++variableIndex) {
const SpvReflectInterfaceVariable* variable = entryPoint.input_variables[variableIndex];
if (variable != nullptr &&
(variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 &&
variable->built_in == SpvBuiltInInstanceIndex) {
return true;
}
}
}
return false;
}
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
switch (stage) {
case ShaderStage::Vertex:
@@ -1465,6 +1472,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
if (!m_shaderDrawParametersEnabled && ReflectedReadsInstanceIndexBuiltin(reflectModule)) {
static Bool s_warnedInstanceIndexUnsupported = false;
if (!s_warnedInstanceIndexUnsupported) {
s_warnedInstanceIndexUnsupported = true;
MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be "
"rebased and instanced draws with a non-zero baseInstance may render incorrectly");
}
}
uint32_t inputCount = 0;
SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr);
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
@@ -1512,6 +1528,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkProgramObject& entry) const {
entry.activeFragmentOutputLocationMask = 0;
entry.fragmentOutputTypes.fill(0);
entry.fragmentReplacesDepth = false;
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
@@ -1533,6 +1550,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
continue;
}
entry.fragmentReplacesDepth = ReflectedFragmentReplacesDepth(reflectModule);
uint32_t outputCount = 0;
SpvReflectResult reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, nullptr);
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
@@ -1873,8 +1892,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
moduleSpirvs[i], invariantSpirv)) {
moduleSpirvs[i] = std::move(invariantSpirv);
} else {
MGLOG_W("ProgramFactory: position-invariant decoration failed for program %u; "
"keeping the original module",
// The pass round-trips through SPIRV-Tools IR, so an unparseable module
// fails open and keeps the undecorated words - which silently reinstates
// the multi-pass invariance bug rather than breaking anything loudly.
MGLOG_E("ProgramFactory: position-invariant decoration failed for program %u; "
"keeping the original module - multi-pass depth-equality chains "
"(e.g. MC 26.3 OIT clouds) may drop primitives on this device",
program.GetExternalIndex());
}
}
@@ -1883,24 +1906,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage
// loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection
// below runs on the rebased words so the added BaseInstance builtin stays consistent.
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) {
if (m_shaderDrawParametersEnabled) {
Vector<Uint> rebasedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
rebasedSpirv)) {
moduleSpirvs[i] = std::move(rebasedSpirv);
} else {
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
"instanced draws with a non-zero baseInstance may render incorrectly",
program.GetExternalIndex());
}
} else if (SpirvDeclaresInstanceIndexBuiltin(moduleSpirvs[i])) {
static Bool s_warnedInstanceIndexUnsupported = false;
if (!s_warnedInstanceIndexUnsupported) {
s_warnedInstanceIndexUnsupported = true;
MGLOG_W("ProgramFactory: shaderDrawParameters is unavailable; gl_InstanceID cannot be "
"rebased and instanced draws with a non-zero baseInstance may render incorrectly");
}
// The unsupported-device counterpart of this rebase (warning when a shader reads
// the builtin but shaderDrawParameters is missing) rides along with
// ReflectVertexInputs, which already reflects this stage.
if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex &&
m_shaderDrawParametersEnabled) {
Vector<Uint> rebasedSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::RebaseInstanceIndexForVulkan(moduleSpirvs[i],
rebasedSpirv)) {
moduleSpirvs[i] = std::move(rebasedSpirv);
} else {
MGLOG_E("ProgramFactory: failed to rebase gl_InstanceID for program %u; "
"instanced draws with a non-zero baseInstance may render incorrectly",
program.GetExternalIndex());
}
}
@@ -78,6 +78,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
Uint32 producerOutputComponentCount = 0;
Uint32 fragmentInputComponentCount = 0;
// The fragment module declares the DepthReplacing execution mode (writes
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
static inline VkDevice s_device = VK_NULL_HANDLE;
@@ -111,6 +115,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
@@ -121,6 +126,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
}
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
if (this == &other) {
@@ -153,6 +159,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rasterizationProducerStage = other.rasterizationProducerStage;
producerOutputComponentCount = other.producerOutputComponentCount;
fragmentInputComponentCount = other.fragmentInputComponentCount;
fragmentReplacesDepth = other.fragmentReplacesDepth;
other.hash = 0;
other.descriptorSetLayout = VK_NULL_HANDLE;
other.pipelineLayout = VK_NULL_HANDLE;
@@ -163,6 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.rasterizationProducerStage = ShaderStage::Unknown;
other.producerOutputComponentCount = 0;
other.fragmentInputComponentCount = 0;
other.fragmentReplacesDepth = false;
return *this;
}
@@ -210,6 +218,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
// True when any entry point declares the DepthReplacing execution mode, i.e. the
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
// can be pinned by tests. A false negative loses the exemption, so such a shader is
// stripped conservatively and forfeits its depth write.
static Bool ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule);
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
private:
struct ProgramLookupCache {
@@ -2112,18 +2112,16 @@ void main() {
// the pipelines of a multi-pass depth-equality chain (even with the SPIR-V
// Invariant decoration), so a blended depth-writing prepass makes later
// equality-compare passes drop whole primitives (MC 26.3 improved-transparency
// clouds flicker black). Suppress blended depth writes there;
// clouds flicker black). Suppress depth writes on accumulation-blended pipelines
// there (see PipelineFactory::ShouldSuppressDepthWrite for the exact scope);
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE forces the quirk on or off on any
// driver.
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
const MG_Config::QuirkOverride quirkOverride =
MG_Config::Features.MagmaDisableBlendedDepthWriteQuirk;
const Bool suppressBlendedDepthWrite =
quirkOverride == MG_Config::QuirkOverride::ForceOn ||
(quirkOverride == MG_Config::QuirkOverride::Auto &&
m_physicalDevice.properties.vendorID == kVendorIdQualcomm);
const Bool suppressBlendedDepthWrite = PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(
quirkOverride, m_physicalDevice.properties.vendorID);
if (suppressBlendedDepthWrite) {
MGLOG_I("DirectVulkan: suppressing depth writes on blended pipelines "
MGLOG_I("DirectVulkan: suppressing depth writes on accumulation-blended pipelines "
"(driver lacks cross-pipeline position invariance)%s",
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
}
@@ -3466,6 +3464,7 @@ void main() {
.backStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthPassOp),
.backStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthFailOp),
.backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func),
.fragmentReplacesDepth = programObj.fragmentReplacesDepth,
.stages = &programObj.stages,
.vertexInputState = pipelineVertexInputState
};
@@ -3670,6 +3669,16 @@ void main() {
"disabling blending on attachments with this format (first hit: attachment %u textureId=%d program=%u)",
static_cast<Int>(colorAttachmentFormat), i, textureExternalIndex,
program.GetExternalIndex());
if (PipelineFactory::IsSuppressBlendedDepthWriteEnabled()) {
// With blending force-disabled the blended depth-write quirk can
// never fire for pipelines on this format, so a depth-equality
// chain that accumulates into it (MC 26.3 OIT depth_bounds on
// RGBA32F) keeps its depth writes and may flicker on this driver.
MGLOG_W("GetOrCreatePipeline: format=%d is not blendable, so the blended "
"depth-write quirk cannot apply to it; depth-equality chains "
"accumulating into this format may flicker",
static_cast<Int>(colorAttachmentFormat));
}
}
}
if (!blendSupportIt->second) {
+2
View File
@@ -72,6 +72,8 @@ add_subdirectory(Texture)
add_subdirectory(VertexArray)
add_subdirectory(Program)
add_subdirectory(Query)
add_subdirectory(Pipeline)
add_subdirectory(ShaderTranspiler)
if (ENABLE_INTEGRATION_TESTS)
add_subdirectory(Backend/DirectVulkan)
endif()
+27
View File
@@ -0,0 +1,27 @@
cmake_minimum_required(VERSION 3.14)
add_executable(
PipelineQuirkTest
PipelineQuirkTest.cpp
)
target_include_directories(PipelineQuirkTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
PipelineQuirkTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(PipelineQuirkTest PRIVATE /Zc:preprocessor)
endif()
include(GoogleTest)
gtest_discover_tests(PipelineQuirkTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,456 @@
// MobileGL - MobileGL/MG_Test/Pipeline/PipelineQuirkTest.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 <gtest/gtest.h>
#include <Config.h>
#include <MG_Backend/DirectVulkan/Renderer/PipelineFactory.h>
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
using namespace MobileGL;
using MobileGL::MG_Backend::DirectVulkan::PipelineFactory;
using MobileGL::MG_Backend::DirectVulkan::ProgramFactory;
using MobileGL::MG_Config::QuirkOverride;
namespace {
constexpr Uint32 kVendorIdQualcomm = 0x5143;
constexpr Uint32 kVendorIdArm = 0x13B5;
constexpr VkColorComponentFlags kFullColorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
// Builds non-separate blend state: the alpha channel repeats the color factors/op, which
// is what glBlendFunc/glBlendEquation (as opposed to their *Separate forms) produce.
// ShouldSuppressDepthWrite deliberately decides on the color channel alone, so these
// cases cover its whole input space; SeparateAlphaAccumulationIsNotStripped below pins
// the separate-alpha contract.
VkPipelineColorBlendAttachmentState MakeBlendAttachment(Bool blendEnable,
VkBlendFactor srcColor,
VkBlendFactor dstColor,
VkBlendOp colorOp,
VkColorComponentFlags colorWriteMask) {
VkPipelineColorBlendAttachmentState attachment{};
attachment.blendEnable = blendEnable ? VK_TRUE : VK_FALSE;
attachment.srcColorBlendFactor = srcColor;
attachment.dstColorBlendFactor = dstColor;
attachment.colorBlendOp = colorOp;
attachment.srcAlphaBlendFactor = srcColor;
attachment.dstAlphaBlendFactor = dstColor;
attachment.alphaBlendOp = colorOp;
attachment.colorWriteMask = colorWriteMask;
return attachment;
}
// glslangValidator -V output for:
// #version 450
// layout(location = 0) out vec4 outColor;
// void main() { outColor = vec4(1.0); gl_FragDepth = 0.5; }
// Assigning gl_FragDepth makes glslang emit OpExecutionMode ... DepthReplacing.
constexpr Uint32 kFragDepthWriterSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000000fu, 0x00000000u, 0x00020011u,
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000004u,
0x00000004u, 0x6e69616du, 0x00000000u, 0x00000009u, 0x0000000du, 0x00030010u,
0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x0000000cu, 0x00030003u,
0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00050005u, 0x00000009u, 0x4374756fu, 0x726f6c6fu, 0x00000000u, 0x00060005u,
0x0000000du, 0x465f6c67u, 0x44676172u, 0x68747065u, 0x00000000u, 0x00040047u,
0x00000009u, 0x0000001eu, 0x00000000u, 0x00040047u, 0x0000000du, 0x0000000bu,
0x00000016u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u,
0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u,
0x00000004u, 0x00040020u, 0x00000008u, 0x00000003u, 0x00000007u, 0x0004003bu,
0x00000008u, 0x00000009u, 0x00000003u, 0x0004002bu, 0x00000006u, 0x0000000au,
0x3f800000u, 0x0007002cu, 0x00000007u, 0x0000000bu, 0x0000000au, 0x0000000au,
0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x00000006u,
0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x0004002bu, 0x00000006u,
0x0000000eu, 0x3f000000u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u,
0x00000003u, 0x000200f8u, 0x00000005u, 0x0003003eu, 0x00000009u, 0x0000000bu,
0x0003003eu, 0x0000000du, 0x0000000eu, 0x000100fdu, 0x00010038u,
};
// Same shader without the gl_FragDepth assignment.
constexpr Uint32 kPlainFragmentSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000000cu, 0x00000000u, 0x00020011u,
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0006000fu, 0x00000004u,
0x00000004u, 0x6e69616du, 0x00000000u, 0x00000009u, 0x00030010u, 0x00000004u,
0x00000007u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u,
0x6e69616du, 0x00000000u, 0x00050005u, 0x00000009u, 0x4374756fu, 0x726f6c6fu,
0x00000000u, 0x00040047u, 0x00000009u, 0x0000001eu, 0x00000000u, 0x00020013u,
0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u,
0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040020u,
0x00000008u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000008u, 0x00000009u,
0x00000003u, 0x0004002bu, 0x00000006u, 0x0000000au, 0x3f800000u, 0x0007002cu,
0x00000007u, 0x0000000bu, 0x0000000au, 0x0000000au, 0x0000000au, 0x0000000au,
0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u,
0x00000005u, 0x0003003eu, 0x00000009u, 0x0000000bu, 0x000100fdu, 0x00010038u,
};
// glslangValidator -V output for a vertex shader reading gl_InstanceIndex:
// #version 450
// layout(location = 0) in vec4 inPos;
// void main() { gl_Position = inPos + vec4(float(gl_InstanceIndex)); }
constexpr Uint32 kInstanceIndexVertexSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001bu, 0x00000000u, 0x00020011u,
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0008000fu, 0x00000000u,
0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00000014u,
0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du,
0x00000000u, 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u,
0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu,
0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu,
0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u,
0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u,
0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du,
0x00000000u, 0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00070005u,
0x00000014u, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x7865646eu, 0x00000000u,
0x00030047u, 0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u,
0x0000000bu, 0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u, 0x0000000bu,
0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u,
0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u,
0x00000011u, 0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x0000000bu,
0x0000002bu, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u,
0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u,
0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu,
0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u,
0x00000009u, 0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au,
0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu,
0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u,
0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u,
0x00000010u, 0x00000001u, 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u,
0x00000001u, 0x00040020u, 0x00000013u, 0x00000001u, 0x0000000eu, 0x0004003bu,
0x00000013u, 0x00000014u, 0x00000001u, 0x00040020u, 0x00000019u, 0x00000003u,
0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u,
0x000200f8u, 0x00000005u, 0x0004003du, 0x00000007u, 0x00000012u, 0x00000011u,
0x0004003du, 0x0000000eu, 0x00000015u, 0x00000014u, 0x0004006fu, 0x00000006u,
0x00000016u, 0x00000015u, 0x00070050u, 0x00000007u, 0x00000017u, 0x00000016u,
0x00000016u, 0x00000016u, 0x00000016u, 0x00050081u, 0x00000007u, 0x00000018u,
0x00000012u, 0x00000017u, 0x00050041u, 0x00000019u, 0x0000001au, 0x0000000du,
0x0000000fu, 0x0003003eu, 0x0000001au, 0x00000018u, 0x000100fdu, 0x00010038u,
};
// Same, but reading gl_VertexIndex instead: a DIFFERENT input builtin. glslang emits
// this for GL's gl_VertexID, so nearly every real vertex shader has one - it is what
// separates "declares some builtin" from "declares the InstanceIndex builtin".
constexpr Uint32 kVertexIndexVertexSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x0000001bu, 0x00000000u, 0x00020011u,
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0008000fu, 0x00000000u,
0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00000014u,
0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du,
0x00000000u, 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u,
0x00000000u, 0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu,
0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu,
0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u,
0x4470696cu, 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u,
0x435f6c67u, 0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du,
0x00000000u, 0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00060005u,
0x00000014u, 0x565f6c67u, 0x65747265u, 0x646e4978u, 0x00007865u, 0x00030047u,
0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu,
0x00000000u, 0x00050048u, 0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u,
0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u,
0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000011u,
0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x0000000bu, 0x0000002au,
0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u,
0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u,
0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u,
0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u,
0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au,
0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu,
0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u,
0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u, 0x00000010u,
0x00000001u, 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u, 0x00000001u,
0x00040020u, 0x00000013u, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x00000013u,
0x00000014u, 0x00000001u, 0x00040020u, 0x00000019u, 0x00000003u, 0x00000007u,
0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u,
0x00000005u, 0x0004003du, 0x00000007u, 0x00000012u, 0x00000011u, 0x0004003du,
0x0000000eu, 0x00000015u, 0x00000014u, 0x0004006fu, 0x00000006u, 0x00000016u,
0x00000015u, 0x00070050u, 0x00000007u, 0x00000017u, 0x00000016u, 0x00000016u,
0x00000016u, 0x00000016u, 0x00050081u, 0x00000007u, 0x00000018u, 0x00000012u,
0x00000017u, 0x00050041u, 0x00000019u, 0x0000001au, 0x0000000du, 0x0000000fu,
0x0003003eu, 0x0000001au, 0x00000018u, 0x000100fdu, 0x00010038u,
};
// Owns the reflection module so each test case cleans up after itself.
class ReflectModule {
public:
template <SizeT WordCount>
explicit ReflectModule(const Uint32 (&spirv)[WordCount]) {
m_created = spvReflectCreateShaderModule(sizeof(spirv), spirv, &m_module) ==
SPV_REFLECT_RESULT_SUCCESS;
}
~ReflectModule() {
if (m_created) {
spvReflectDestroyShaderModule(&m_module);
}
}
ReflectModule(const ReflectModule&) = delete;
ReflectModule& operator=(const ReflectModule&) = delete;
Bool Created() const { return m_created; }
const SpvReflectShaderModule& Get() const { return m_module; }
private:
SpvReflectShaderModule m_module{};
Bool m_created = false;
};
PipelineFactory::PipelineCreatePayload MakeDepthWritingPayload(
const VkPipelineColorBlendAttachmentState& attachment0) {
PipelineFactory::PipelineCreatePayload payload{};
payload.colorAttachmentCount = 1;
payload.depthTestEnable = true;
payload.depthWriteEnable = true;
payload.colorBlendAttachments[0] = attachment0;
return payload;
}
} // namespace
// --- Device gate: MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE tri-state ---
TEST(PipelineQuirkDeviceGate, ForceOnEnablesOnAnyVendor) {
EXPECT_TRUE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOn,
kVendorIdArm));
EXPECT_TRUE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOn,
kVendorIdQualcomm));
}
TEST(PipelineQuirkDeviceGate, ForceOffDisablesEvenOnQualcomm) {
EXPECT_FALSE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOff,
kVendorIdQualcomm));
}
TEST(PipelineQuirkDeviceGate, AutoDetectsQualcommOnly) {
EXPECT_TRUE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::Auto,
kVendorIdQualcomm));
EXPECT_FALSE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::Auto,
kVendorIdArm));
}
TEST(PipelineQuirkDeviceGate, ForceOnRoundTripsThroughTheFactoryFlag) {
const Bool previous = PipelineFactory::IsSuppressBlendedDepthWriteEnabled();
PipelineFactory::SetSuppressBlendedDepthWrite(
PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOn, kVendorIdArm));
EXPECT_TRUE(PipelineFactory::IsSuppressBlendedDepthWriteEnabled());
PipelineFactory::SetSuppressBlendedDepthWrite(previous);
}
// --- Per-pipeline strip decision against the pipeline create-info payload ---
TEST(PipelineQuirkStripDecision, MaxBlendIsStripped) {
// MC 26.3 OIT depth_bounds: GL_MAX accumulation writing depth - the case the quirk fixes.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_MAX, kFullColorWriteMask));
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, MinBlendIsStripped) {
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_MIN, kFullColorWriteMask));
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, AdditiveOnePlusOneIsStripped) {
// MC 26.3 OIT transmittance/accumulate: ONE+ONE additive accumulation writing depth.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask));
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, SortedTransparencyOverBlendIsNotStripped) {
// Vanilla MC translucent layer (water, stained glass): SRC_ALPHA "over" compositing
// draws each surface once and depends on its depth writes to occlude particles, rain,
// and clouds drawn later - it must keep them.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD,
kFullColorWriteMask));
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, EffectivelyOpaqueBlendIsNotStripped) {
// GL_BLEND left enabled with ONE/ZERO+ADD factors is opaque in effect; stripping its
// depth write would break occlusion for plainly opaque geometry.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kFullColorWriteMask));
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, FullyMaskedAccumulationBlendIsNotStripped) {
// Depth-prepass pattern: colorMask(0,0,0,0) with blending left enabled - blending is
// moot, and stripping would delete the entire prepass.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 0));
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, DisabledBlendIsNotStripped) {
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask));
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, NoDepthWriteMeansNoStrip) {
auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask));
payload.depthWriteEnable = false;
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, FragDepthWriterIsExempt) {
// gl_FragDepth output does not go through per-pipeline vertex position math, so the
// cross-pipeline invariance hazard cannot affect it (e.g. the 26.3 OIT composite).
auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask));
payload.fragmentReplacesDepth = true;
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) {
// The hazard is not limited to attachment 0: the 26.3 transmittance pass accumulates
// into a 2-target MRT.
PipelineFactory::PipelineCreatePayload payload{};
payload.colorAttachmentCount = 2;
payload.depthTestEnable = true;
payload.depthWriteEnable = true;
payload.colorBlendAttachments[0] = MakeBlendAttachment(
false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kFullColorWriteMask);
payload.colorBlendAttachments[1] = MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask);
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, AlphaWeightedAdditiveIsNotStripped) {
// SRC_ALPHA,ONE additive is order-independent in the color channel but is the classic
// *sorted* particle/glow blend, not an OIT accumulation pass. Pins the src==ONE clause:
// without it this state would be stripped.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask));
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, ReverseSubtractIsNotStripped) {
// Deliberate narrowing: only MIN/MAX and ONE+ONE ADD carry the equality-chain
// signature. SUBTRACT-class ops stay outside the quirk until content demands them.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_REVERSE_SUBTRACT,
kFullColorWriteMask));
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, PartiallyMaskedAccumulationIsStripped) {
// Only a fully masked attachment is exempt; a live alpha channel still accumulates.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_COLOR_COMPONENT_A_BIT));
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, NoColorAttachmentsMeansNoStrip) {
// Depth-only FBO: the loop must not read the (stale) attachment array at all.
PipelineFactory::PipelineCreatePayload payload{};
payload.colorAttachmentCount = 0;
payload.depthTestEnable = true;
payload.depthWriteEnable = true;
payload.colorBlendAttachments[0] = MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask);
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
TEST(PipelineQuirkStripDecision, SeparateAlphaAccumulationIsNotStripped) {
// glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX) over an ordinary color over-blend: the
// alpha channel accumulates but the color channel does not. Pins that the decision is
// color-channel only - widening it to alpha would re-capture sorted transparency.
auto attachment = MakeBlendAttachment(true, VK_BLEND_FACTOR_SRC_ALPHA,
VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD,
kFullColorWriteMask);
attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
attachment.alphaBlendOp = VK_BLEND_OP_MAX;
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(MakeDepthWritingPayload(attachment)));
}
TEST(PipelineQuirkStripDecision, MixedOverAndMaskedAttachmentsAreNotStripped) {
PipelineFactory::PipelineCreatePayload payload{};
payload.colorAttachmentCount = 2;
payload.depthTestEnable = true;
payload.depthWriteEnable = true;
payload.colorBlendAttachments[0] = MakeBlendAttachment(
true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD,
kFullColorWriteMask);
payload.colorBlendAttachments[1] = MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, 0);
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
// --- DepthReplacing reflection feeding the gl_FragDepth exemption ---
TEST(ReflectedFragmentReplacesDepth, TrueForAShaderThatAssignsFragDepth) {
const ReflectModule module(kFragDepthWriterSpirv);
ASSERT_TRUE(module.Created());
EXPECT_TRUE(ProgramFactory::ReflectedFragmentReplacesDepth(module.Get()));
}
TEST(ReflectedFragmentReplacesDepth, FalseForAPlainFragmentShader) {
const ReflectModule module(kPlainFragmentSpirv);
ASSERT_TRUE(module.Created());
EXPECT_FALSE(ProgramFactory::ReflectedFragmentReplacesDepth(module.Get()));
}
TEST(ReflectedFragmentReplacesDepth, FalseForAnEmptyModule) {
// A default-constructed module has no entry points; the scan must not dereference.
SpvReflectShaderModule emptyModule{};
EXPECT_FALSE(ProgramFactory::ReflectedFragmentReplacesDepth(emptyModule));
}
TEST(ReflectedFragmentReplacesDepth, ReflectedFlagFlipsTheStripDecision) {
// The two fixtures differ only by the gl_FragDepth assignment, so they pin that the
// reflected flag is what flips the strip decision for an otherwise identical pipeline.
const ReflectModule depthWriter(kFragDepthWriterSpirv);
const ReflectModule plain(kPlainFragmentSpirv);
ASSERT_TRUE(depthWriter.Created());
ASSERT_TRUE(plain.Created());
auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask));
payload.fragmentReplacesDepth = ProgramFactory::ReflectedFragmentReplacesDepth(plain.Get());
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
payload.fragmentReplacesDepth = ProgramFactory::ReflectedFragmentReplacesDepth(depthWriter.Get());
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
}
// --- InstanceIndex reflection feeding the shaderDrawParameters diagnostic ---
TEST(ReflectedReadsInstanceIndexBuiltin, TrueForAShaderReadingInstanceIndex) {
const ReflectModule module(kInstanceIndexVertexSpirv);
ASSERT_TRUE(module.Created());
EXPECT_TRUE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(module.Get()));
}
TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAShaderReadingADifferentBuiltin) {
// Discriminates the builtin's identity, not merely its presence: weakening the check to
// "has any BuiltIn decoration" would fire the diagnostic on every real vertex shader.
const ReflectModule module(kVertexIndexVertexSpirv);
ASSERT_TRUE(module.Created());
EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(module.Get()));
}
TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAShaderWithNoInputBuiltins) {
const ReflectModule module(kPlainFragmentSpirv);
ASSERT_TRUE(module.Created());
EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(module.Get()));
}
TEST(ReflectedReadsInstanceIndexBuiltin, FalseForAnEmptyModule) {
SpvReflectShaderModule emptyModule{};
EXPECT_FALSE(ProgramFactory::ReflectedReadsInstanceIndexBuiltin(emptyModule));
}
@@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.14)
add_executable(
SpirvPassTest
SpirvPassTest.cpp
)
target_include_directories(SpirvPassTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(
SpirvPassTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(SpirvPassTest PRIVATE /Zc:preprocessor)
endif()
include(GoogleTest)
gtest_discover_tests(SpirvPassTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,170 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/SpirvPassTest.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 <gtest/gtest.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <spirv_reflect.h>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
// glslangValidator -V output. Both are vertex shaders writing gl_Position through
// the gl_PerVertex block, i.e. the Position builtin arrives as OpMemberDecorate rather
// than a plain OpDecorate - the shape real glslang output actually takes.
// #version 450
// layout(location = 0) in vec4 inPos;
// void main() { gl_Position = inPos; }
constexpr Uint32 kPlainVertexSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000015u, 0x00000000u, 0x00020011u,
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000000u,
0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00030003u,
0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u,
0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u,
0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u,
0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u,
0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u,
0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00030047u, 0x0000000bu,
0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u,
0x00050048u, 0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u,
0x0000000bu, 0x00000002u, 0x0000000bu, 0x00000003u, 0x00050048u, 0x0000000bu,
0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000011u, 0x0000001eu,
0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u,
0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u,
0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu,
0x00000008u, 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u,
0x00000009u, 0x0006001eu, 0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au,
0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu,
0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u,
0x00000001u, 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u,
0x00000010u, 0x00000001u, 0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u,
0x00000001u, 0x00040020u, 0x00000013u, 0x00000003u, 0x00000007u, 0x00050036u,
0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u,
0x0004003du, 0x00000007u, 0x00000012u, 0x00000011u, 0x00050041u, 0x00000013u,
0x00000014u, 0x0000000du, 0x0000000fu, 0x0003003eu, 0x00000014u, 0x00000012u,
0x000100fdu, 0x00010038u,
};
// ... plus `invariant gl_Position;` - already carries OpMemberDecorate %gl_PerVertex 0
// Invariant, so the pass must not add a duplicate.
constexpr Uint32 kAlreadyInvariantVertexSpirv[] = {
0x07230203u, 0x00010000u, 0x0008000bu, 0x00000015u, 0x00000000u, 0x00020011u,
0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu,
0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000000u,
0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000011u, 0x00030003u,
0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u,
0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u,
0x00060006u, 0x0000000bu, 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u,
0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, 0x746e696fu, 0x657a6953u,
0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu,
0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u,
0x446c6c75u, 0x61747369u, 0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u,
0x00040005u, 0x00000011u, 0x6f506e69u, 0x00000073u, 0x00030047u, 0x0000000bu,
0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u,
0x00040048u, 0x0000000bu, 0x00000000u, 0x00000012u, 0x00050048u, 0x0000000bu,
0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u,
0x0000000bu, 0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu,
0x00000004u, 0x00040047u, 0x00000011u, 0x0000001eu, 0x00000000u, 0x00020013u,
0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u,
0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040015u,
0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u, 0x00000009u,
0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu,
0x0000000bu, 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u,
0x0000000cu, 0x00000003u, 0x0000000bu, 0x0004003bu, 0x0000000cu, 0x0000000du,
0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u, 0x0004002bu,
0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040020u, 0x00000010u, 0x00000001u,
0x00000007u, 0x0004003bu, 0x00000010u, 0x00000011u, 0x00000001u, 0x00040020u,
0x00000013u, 0x00000003u, 0x00000007u, 0x00050036u, 0x00000002u, 0x00000004u,
0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000007u,
0x00000012u, 0x00000011u, 0x00050041u, 0x00000013u, 0x00000014u, 0x0000000du,
0x0000000fu, 0x0003003eu, 0x00000014u, 0x00000012u, 0x000100fdu, 0x00010038u,
};
// OpMemberDecorate <struct-id> <member> <decoration>
constexpr Uint32 kOpMemberDecorate = 72;
constexpr Uint32 kDecorationInvariant = 18;
constexpr Uint32 kSpirvHeaderWordCount = 5;
// Test-side reference walker. Deliberately independent of the production code so a bug in
// the pass cannot hide behind the same helper; only used to count what the pass emitted.
Uint32 CountInvariantMemberDecorations(const Vector<Uint32>& spirv) {
Uint32 count = 0;
for (SizeT i = kSpirvHeaderWordCount; i < spirv.size();) {
const Uint32 wordCount = spirv[i] >> 16;
const Uint32 opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) {
break;
}
if (opcode == kOpMemberDecorate && wordCount >= 4 && spirv[i + 3] == kDecorationInvariant) {
++count;
}
i += wordCount;
}
return count;
}
template <SizeT WordCount>
Vector<Uint32> ToVector(const Uint32 (&words)[WordCount]) {
return Vector<Uint32>(words, words + WordCount);
}
} // namespace
// --- DecoratePositionInvariantPass ---
TEST(DecoratePositionInvariant, AddsInvariantToThePositionMember) {
const Vector<Uint32> input = ToVector(kPlainVertexSpirv);
ASSERT_EQ(CountInvariantMemberDecorations(input), 0u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(input, output));
EXPECT_EQ(CountInvariantMemberDecorations(output), 1u);
}
TEST(DecoratePositionInvariant, DoesNotDuplicateAnExistingInvariant) {
const Vector<Uint32> input = ToVector(kAlreadyInvariantVertexSpirv);
ASSERT_EQ(CountInvariantMemberDecorations(input), 1u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(input, output));
EXPECT_EQ(CountInvariantMemberDecorations(output), 1u);
// The pass reports SuccessWithoutChange here, and SPIRV-Tools asserts (in assert-enabled
// builds) that such a run round-trips byte-identically. Pin that from the outside so an
// assert-enabled CI build cannot be the first thing to discover a violation.
EXPECT_EQ(output, input);
}
TEST(DecoratePositionInvariant, IsIdempotent) {
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(ToVector(kPlainVertexSpirv), once));
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(once, twice));
EXPECT_EQ(CountInvariantMemberDecorations(twice), 1u);
}
TEST(DecoratePositionInvariant, OutputStaysAReflectableModule) {
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::DecoratePositionInvariantForVulkan(ToVector(kPlainVertexSpirv), output));
SpvReflectShaderModule module{};
ASSERT_EQ(spvReflectCreateShaderModule(output.size() * sizeof(Uint32), output.data(), &module),
SPV_REFLECT_RESULT_SUCCESS);
EXPECT_EQ(module.entry_point_count, 1u);
spvReflectDestroyShaderModule(&module);
}
TEST(DecoratePositionInvariant, RejectsGarbageInput) {
const Vector<Uint32> notSpirv{0xdeadbeefu, 0u, 0u, 0u, 0u};
Vector<Uint32> output;
EXPECT_FALSE(ShaderCompiler::DecoratePositionInvariantForVulkan(notSpirv, output));
}
@@ -16,6 +16,7 @@
#include "SpirvPasses/FlattenInterfaceStructPass.h"
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
#include "SpirvPasses/DecoratePositionInvariantPass.h"
#include "SpirvPasses/LowerDrawParametersPass.h"
#include "SpirvPasses/RebaseInstanceIndexPass.h"
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
@@ -347,71 +348,14 @@ namespace MobileGL {
bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
static constexpr Uint32 kHeaderWords = 5;
static constexpr Uint32 kOpDecorate = 71;
static constexpr Uint32 kOpMemberDecorate = 72;
static constexpr Uint32 kDecorationInvariant = 18;
static constexpr Uint32 kDecorationBuiltIn = 11;
static constexpr Uint32 kBuiltInPosition = 0;
if (inputBinary.size() < kHeaderWords) {
return false;
}
using namespace spvtools;
OptimizerOptions options;
options.set_run_validator(false);
// First pass: find targets that already carry Invariant so we never duplicate.
struct MemberKey {
Uint32 id;
Uint32 member;
bool operator==(const MemberKey& o) const { return id == o.id && member == o.member; }
};
Vector<Uint32> invariantIds;
Vector<MemberKey> invariantMembers;
for (SizeT i = kHeaderWords; i < inputBinary.size();) {
const Uint32 word0 = inputBinary[i];
const Uint32 opcode = word0 & 0xFFFFu;
const Uint32 length = word0 >> 16;
if (length == 0 || i + length > inputBinary.size()) {
return false;
}
if (opcode == kOpDecorate && length >= 3 && inputBinary[i + 2] == kDecorationInvariant) {
invariantIds.push_back(inputBinary[i + 1]);
} else if (opcode == kOpMemberDecorate && length >= 4 &&
inputBinary[i + 3] == kDecorationInvariant) {
invariantMembers.push_back({inputBinary[i + 1], inputBinary[i + 2]});
}
i += length;
}
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
optimizer.RegisterPass(DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass());
outputBinary.clear();
outputBinary.reserve(inputBinary.size() + 8);
outputBinary.insert(outputBinary.end(), inputBinary.begin(), inputBinary.begin() + kHeaderWords);
for (SizeT i = kHeaderWords; i < inputBinary.size();) {
const Uint32 word0 = inputBinary[i];
const Uint32 opcode = word0 & 0xFFFFu;
const Uint32 length = word0 >> 16;
outputBinary.insert(outputBinary.end(), inputBinary.begin() + i,
inputBinary.begin() + i + length);
if (opcode == kOpDecorate && length == 4 &&
inputBinary[i + 2] == kDecorationBuiltIn && inputBinary[i + 3] == kBuiltInPosition) {
const Uint32 target = inputBinary[i + 1];
if (std::find(invariantIds.begin(), invariantIds.end(), target) == invariantIds.end()) {
outputBinary.push_back((3u << 16) | kOpDecorate);
outputBinary.push_back(target);
outputBinary.push_back(kDecorationInvariant);
}
} else if (opcode == kOpMemberDecorate && length == 5 &&
inputBinary[i + 3] == kDecorationBuiltIn && inputBinary[i + 4] == kBuiltInPosition) {
const MemberKey key{inputBinary[i + 1], inputBinary[i + 2]};
if (std::find(invariantMembers.begin(), invariantMembers.end(), key) ==
invariantMembers.end()) {
outputBinary.push_back((4u << 16) | kOpMemberDecorate);
outputBinary.push_back(key.id);
outputBinary.push_back(key.member);
outputBinary.push_back(kDecorationInvariant);
}
}
i += length;
}
return true;
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
}
bool ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan(
@@ -0,0 +1,139 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.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 "DecoratePositionInvariantPass.h"
#include "spirv.hpp"
#include "source/opt/instruction.h"
#include "source/opt/ir_context.h"
#include "source/util/make_unique.h"
#include <algorithm>
#include <vector>
namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
namespace {
using spvtools::opt::Instruction;
using spvtools::opt::IRContext;
using spvtools::opt::Operand;
// Identifies one member of a decorated struct (gl_PerVertex's Position slot).
struct MemberKey {
uint32_t id = 0;
uint32_t member = 0;
bool operator==(const MemberKey& other) const {
return id == other.id && member == other.member;
}
};
// OpDecorate <target-id> <decoration> [literals...]
// OpMemberDecorate <struct-id> <member> <decoration> [literals...]
constexpr uint32_t kDecorateTargetOperand = 0;
constexpr uint32_t kDecorateDecorationOperand = 1;
constexpr uint32_t kDecorateBuiltInOperand = 2;
constexpr uint32_t kMemberDecorateStructOperand = 0;
constexpr uint32_t kMemberDecorateMemberOperand = 1;
constexpr uint32_t kMemberDecorateDecorationOperand = 2;
constexpr uint32_t kMemberDecorateBuiltInOperand = 3;
} // namespace
spvtools::opt::Pass::Status DecoratePositionInvariantPass::Process() {
auto* irContext = context();
// Collect first: AddAnnotationInst mutates the list being walked.
std::vector<uint32_t> invariantIds;
std::vector<MemberKey> invariantMembers;
std::vector<uint32_t> positionIds;
std::vector<MemberKey> positionMembers;
for (const Instruction& annotation : irContext->annotations()) {
if (annotation.opcode() == spv::Op::OpDecorate) {
if (annotation.NumInOperands() <= kDecorateDecorationOperand) {
continue;
}
const auto decoration = static_cast<spv::Decoration>(
annotation.GetSingleWordInOperand(kDecorateDecorationOperand));
const uint32_t target = annotation.GetSingleWordInOperand(kDecorateTargetOperand);
if (decoration == spv::Decoration::Invariant) {
invariantIds.push_back(target);
} else if (decoration == spv::Decoration::BuiltIn &&
annotation.NumInOperands() > kDecorateBuiltInOperand &&
static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(
kDecorateBuiltInOperand)) == spv::BuiltIn::Position) {
positionIds.push_back(target);
}
} else if (annotation.opcode() == spv::Op::OpMemberDecorate) {
if (annotation.NumInOperands() <= kMemberDecorateDecorationOperand) {
continue;
}
const auto decoration = static_cast<spv::Decoration>(
annotation.GetSingleWordInOperand(kMemberDecorateDecorationOperand));
const MemberKey key{
annotation.GetSingleWordInOperand(kMemberDecorateStructOperand),
annotation.GetSingleWordInOperand(kMemberDecorateMemberOperand)};
if (decoration == spv::Decoration::Invariant) {
invariantMembers.push_back(key);
} else if (decoration == spv::Decoration::BuiltIn &&
annotation.NumInOperands() > kMemberDecorateBuiltInOperand &&
static_cast<spv::BuiltIn>(annotation.GetSingleWordInOperand(
kMemberDecorateBuiltInOperand)) == spv::BuiltIn::Position) {
positionMembers.push_back(key);
}
}
}
Bool changed = false;
for (const uint32_t target : positionIds) {
if (std::find(invariantIds.begin(), invariantIds.end(), target) != invariantIds.end()) {
continue;
}
irContext->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpDecorate, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {target}},
{SPV_OPERAND_TYPE_DECORATION,
{static_cast<uint32_t>(spv::Decoration::Invariant)}}}));
// Guard against a second Position decoration on the same target.
invariantIds.push_back(target);
changed = true;
}
for (const MemberKey& key : positionMembers) {
if (std::find(invariantMembers.begin(), invariantMembers.end(), key) !=
invariantMembers.end()) {
continue;
}
irContext->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
irContext, spv::Op::OpMemberDecorate, 0, 0,
std::initializer_list<Operand>{
{SPV_OPERAND_TYPE_ID, {key.id}},
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {key.member}},
{SPV_OPERAND_TYPE_DECORATION,
{static_cast<uint32_t>(spv::Decoration::Invariant)}}}));
invariantMembers.push_back(key);
changed = true;
}
if (!changed) {
return Status::SuccessWithoutChange;
}
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
return Status::SuccessWithChange;
}
spvtools::Optimizer::PassToken
DecoratePositionInvariantPass::CreateDecoratePositionInvariantPass() {
return spvtools::Optimizer::PassToken(MakeUnique<DecoratePositionInvariantPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -0,0 +1,35 @@
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.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 {
// Adds the Invariant decoration to every Position builtin output. GL apps
// routinely rely on cross-program position invariance for multi-pass equality
// depth tests - MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the
// depth its own first pass wrote - and a driver that optimizes each pipeline
// separately may otherwise vary the position math between passes, dropping whole
// primitives from the later ones. Both the plain (OpDecorate on a Position
// variable) and the block-member (OpMemberDecorate on gl_PerVertex) spellings are
// handled; targets that already carry Invariant are left alone. DirectVulkan only.
class DecoratePositionInvariantPass : public spvtools::opt::Pass {
public:
const char* name() const override { return "decorate-position-invariant"; }
Status Process() override;
static spvtools::Optimizer::PassToken CreateDecoratePositionInvariantPass();
};
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
+2 -1
View File
@@ -279,7 +279,8 @@
"trace_archive": "improved-transparency-minecraft-26.3.tgz",
"golden": "improved-transparency-minecraft-26.3.0002667619.png",
"target_call": 2667619,
"timeout_seconds": 1800
"timeout_seconds": 1800,
"ssim_threshold": 0.995
},
{
"name": "minecraft-1.21.4-fabric-iris-iterationrp-in-world",