mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Fix] (DirectVulkan): narrow the blended depth-write quirk to order-independent accumulation blends, exempting sorted-transparency, gl_FragDepth writers and fully masked attachments
This commit is contained in:
+5
-3
@@ -81,9 +81,11 @@ namespace MobileGL::MG_Config {
|
|||||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||||
// strips depth writes from blended pipelines on drivers without cross-pipeline
|
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||||
// vertex position invariance (see VulkanRenderer's PipelineFactory setup). Auto
|
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||||
// detects Qualcomm.
|
// 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;
|
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
|
||||||
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
// 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
|
// 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");
|
"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) {
|
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
|
||||||
s_suppressBlendedDepthWrite = 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() {
|
PipelineFactory::~PipelineFactory() {
|
||||||
DestroyAll();
|
DestroyAll();
|
||||||
if (m_pipelineCache != VK_NULL_HANDLE) {
|
if (m_pipelineCache != VK_NULL_HANDLE) {
|
||||||
@@ -157,6 +228,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
|
||||||
XXHASH_VERIFY(
|
XXHASH_VERIFY(
|
||||||
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
|
||||||
|
XXHASH_VERIFY(
|
||||||
|
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
|
||||||
if (payload.colorAttachmentCount > 0) {
|
if (payload.colorAttachmentCount > 0) {
|
||||||
XXHASH_VERIFY(XXH64_update(
|
XXHASH_VERIFY(XXH64_update(
|
||||||
m_hashState,
|
m_hashState,
|
||||||
@@ -263,17 +336,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||||
colorAttachments[i] = payload.colorBlendAttachments[i];
|
colorAttachments[i] = payload.colorBlendAttachments[i];
|
||||||
}
|
}
|
||||||
// Suppress depth writes on blended pipelines when the active driver cannot keep
|
// Suppress depth writes on accumulation-blended pipelines when the active driver
|
||||||
// vertex positions invariant across the pipelines of a multi-pass depth-equality
|
// cannot keep vertex positions invariant across the pipelines of a multi-pass
|
||||||
// chain (see SetSuppressBlendedDepthWrite). Blended draws that write depth are rare
|
// depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed
|
||||||
// and the equality-dependent prepass pattern is exactly the case that breaks.
|
// in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water),
|
||||||
if (s_suppressBlendedDepthWrite && depthStencil.depthWriteEnable == VK_TRUE) {
|
// gl_FragDepth writers, and masked-out attachments keep their depth writes.
|
||||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
// This bakes the decision into the pipeline, which only works because depth write is
|
||||||
if (colorAttachments[i].blendEnable == VK_TRUE) {
|
// static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates
|
||||||
depthStencil.depthWriteEnable = VK_FALSE;
|
// would let the record-time value override it and silently disable the quirk.
|
||||||
break;
|
if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) {
|
||||||
}
|
depthStencil.depthWriteEnable = VK_FALSE;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||||
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
|
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
|
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
|
||||||
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
|
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
|
||||||
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS;
|
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{};
|
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||||
@@ -62,13 +65,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
|
||||||
void DestroyAll();
|
void DestroyAll();
|
||||||
|
|
||||||
// Driver quirk: suppress depth writes on blended pipelines. Multi-pass depth-equality
|
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
|
||||||
// rendering (a blended prepass writes depth that later passes re-test with an
|
// depth-equality rendering (a blended prepass writes depth that later passes re-test
|
||||||
// equality-inclusive compare on the re-rasterized geometry) requires cross-pipeline
|
// with an equality-inclusive compare on the re-rasterized geometry) requires
|
||||||
// position invariance that some mobile compilers do not provide, even with the
|
// cross-pipeline position invariance that some mobile compilers do not provide, even
|
||||||
// SPIR-V Invariant decoration; whole primitives then drop out of the later passes.
|
// with the SPIR-V Invariant decoration; whole primitives then drop out of the later
|
||||||
// Set at renderer initialization based on the active driver.
|
// 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 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:
|
private:
|
||||||
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
|
||||||
|
|||||||
@@ -1218,6 +1218,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // 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;
|
||||||
|
}
|
||||||
|
|
||||||
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
|
VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) {
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case ShaderStage::Vertex:
|
case ShaderStage::Vertex:
|
||||||
@@ -1512,6 +1528,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
VkProgramObject& entry) const {
|
VkProgramObject& entry) const {
|
||||||
entry.activeFragmentOutputLocationMask = 0;
|
entry.activeFragmentOutputLocationMask = 0;
|
||||||
entry.fragmentOutputTypes.fill(0);
|
entry.fragmentOutputTypes.fill(0);
|
||||||
|
entry.fragmentReplacesDepth = false;
|
||||||
|
|
||||||
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) {
|
||||||
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
|
if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) {
|
||||||
@@ -1533,6 +1550,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.fragmentReplacesDepth = ReflectedFragmentReplacesDepth(reflectModule);
|
||||||
|
|
||||||
uint32_t outputCount = 0;
|
uint32_t outputCount = 0;
|
||||||
SpvReflectResult reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, nullptr);
|
SpvReflectResult reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, nullptr);
|
||||||
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
|
MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS,
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
|
ShaderStage rasterizationProducerStage = ShaderStage::Unknown;
|
||||||
Uint32 producerOutputComponentCount = 0;
|
Uint32 producerOutputComponentCount = 0;
|
||||||
Uint32 fragmentInputComponentCount = 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;
|
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||||
|
|
||||||
@@ -111,6 +115,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
@@ -121,6 +126,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.rasterizationProducerStage = ShaderStage::Unknown;
|
other.rasterizationProducerStage = ShaderStage::Unknown;
|
||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
|
other.fragmentReplacesDepth = false;
|
||||||
}
|
}
|
||||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||||
if (this == &other) {
|
if (this == &other) {
|
||||||
@@ -153,6 +159,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
rasterizationProducerStage = other.rasterizationProducerStage;
|
rasterizationProducerStage = other.rasterizationProducerStage;
|
||||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||||
|
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||||
other.hash = 0;
|
other.hash = 0;
|
||||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||||
other.pipelineLayout = VK_NULL_HANDLE;
|
other.pipelineLayout = VK_NULL_HANDLE;
|
||||||
@@ -163,6 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
other.rasterizationProducerStage = ShaderStage::Unknown;
|
other.rasterizationProducerStage = ShaderStage::Unknown;
|
||||||
other.producerOutputComponentCount = 0;
|
other.producerOutputComponentCount = 0;
|
||||||
other.fragmentInputComponentCount = 0;
|
other.fragmentInputComponentCount = 0;
|
||||||
|
other.fragmentReplacesDepth = false;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +218,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
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);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct ProgramLookupCache {
|
struct ProgramLookupCache {
|
||||||
|
|||||||
@@ -2112,18 +2112,16 @@ void main() {
|
|||||||
// the pipelines of a multi-pass depth-equality chain (even with the SPIR-V
|
// the pipelines of a multi-pass depth-equality chain (even with the SPIR-V
|
||||||
// Invariant decoration), so a blended depth-writing prepass makes later
|
// Invariant decoration), so a blended depth-writing prepass makes later
|
||||||
// equality-compare passes drop whole primitives (MC 26.3 improved-transparency
|
// 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
|
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE forces the quirk on or off on any
|
||||||
// driver.
|
// driver.
|
||||||
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
|
|
||||||
const MG_Config::QuirkOverride quirkOverride =
|
const MG_Config::QuirkOverride quirkOverride =
|
||||||
MG_Config::Features.MagmaDisableBlendedDepthWriteQuirk;
|
MG_Config::Features.MagmaDisableBlendedDepthWriteQuirk;
|
||||||
const Bool suppressBlendedDepthWrite =
|
const Bool suppressBlendedDepthWrite = PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(
|
||||||
quirkOverride == MG_Config::QuirkOverride::ForceOn ||
|
quirkOverride, m_physicalDevice.properties.vendorID);
|
||||||
(quirkOverride == MG_Config::QuirkOverride::Auto &&
|
|
||||||
m_physicalDevice.properties.vendorID == kVendorIdQualcomm);
|
|
||||||
if (suppressBlendedDepthWrite) {
|
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",
|
"(driver lacks cross-pipeline position invariance)%s",
|
||||||
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
|
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
|
||||||
}
|
}
|
||||||
@@ -3466,6 +3464,7 @@ void main() {
|
|||||||
.backStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthPassOp),
|
.backStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthPassOp),
|
||||||
.backStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthFailOp),
|
.backStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthFailOp),
|
||||||
.backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func),
|
.backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func),
|
||||||
|
.fragmentReplacesDepth = programObj.fragmentReplacesDepth,
|
||||||
.stages = &programObj.stages,
|
.stages = &programObj.stages,
|
||||||
.vertexInputState = pipelineVertexInputState
|
.vertexInputState = pipelineVertexInputState
|
||||||
};
|
};
|
||||||
@@ -3670,6 +3669,16 @@ void main() {
|
|||||||
"disabling blending on attachments with this format (first hit: attachment %u textureId=%d program=%u)",
|
"disabling blending on attachments with this format (first hit: attachment %u textureId=%d program=%u)",
|
||||||
static_cast<Int>(colorAttachmentFormat), i, textureExternalIndex,
|
static_cast<Int>(colorAttachmentFormat), i, textureExternalIndex,
|
||||||
program.GetExternalIndex());
|
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) {
|
if (!blendSupportIt->second) {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ add_subdirectory(Texture)
|
|||||||
add_subdirectory(VertexArray)
|
add_subdirectory(VertexArray)
|
||||||
add_subdirectory(Program)
|
add_subdirectory(Program)
|
||||||
add_subdirectory(Query)
|
add_subdirectory(Query)
|
||||||
|
add_subdirectory(Pipeline)
|
||||||
if (ENABLE_INTEGRATION_TESTS)
|
if (ENABLE_INTEGRATION_TESTS)
|
||||||
add_subdirectory(Backend/DirectVulkan)
|
add_subdirectory(Backend/DirectVulkan)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -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,341 @@
|
|||||||
|
// 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user