mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
Compare commits
9
Commits
86b02b59ed
...
e724e88eec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e724e88eec | ||
|
|
3b175fb88a | ||
|
|
b5a4e7075a | ||
|
|
520c2b6750 | ||
|
|
57cc652b1d | ||
|
|
65ea54da9e | ||
|
|
c81dd04f08 | ||
|
|
c158bfa584 | ||
|
|
f9f455144c |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -80,6 +80,13 @@ namespace MobileGL::MG_Config {
|
||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||
// 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 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
|
||||
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||
|
||||
@@ -135,6 +135,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
}
|
||||
|
||||
|
||||
@@ -2216,11 +2216,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) {
|
||||
// Multisample targets reject the *sampler* parameters (LOD range, border color) but
|
||||
// GL_TEXTURE_SWIZZLE_* is texture state, not sampler state, and ES accepts it on them.
|
||||
// Bailing out entirely used to drop every swizzle write on the floor, which is what the
|
||||
// frontend already assumes is legal (see GL_Texture.cpp's MS-invalid pname list, which
|
||||
// deliberately omits the swizzle enums). Note the caches for the skipped parameters are
|
||||
// still refreshed so they never look stale, but m_cacheSwizzleParams must NOT be, or the
|
||||
// change detection below would swallow the very writes we came here to emit.
|
||||
const Bool isMultisampleTarget = TextureImpl::IsMultisampleTextureTarget(targetInternal);
|
||||
if (isMultisampleTarget) {
|
||||
m_cacheLodRange = stateTextureObject->GetLevelRange();
|
||||
m_cacheSwizzleParams = stateTextureObject->GetAllSwizzleParams();
|
||||
m_cacheBorderColor = stateTextureObject->GetBorderColor();
|
||||
return;
|
||||
}
|
||||
|
||||
Bind(target);
|
||||
@@ -2233,14 +2239,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
const auto& levelRange = stateTextureObject->GetLevelRange();
|
||||
|
||||
if (m_cacheLodRange.x() != levelRange.x()) {
|
||||
if (!isMultisampleTarget && m_cacheLodRange.x() != levelRange.x()) {
|
||||
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_BASE_LEVEL, static_cast<GLint>(levelRange.x()));
|
||||
m_cacheLodRange.x() = levelRange.x();
|
||||
}
|
||||
DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) {
|
||||
MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str());
|
||||
});
|
||||
if (m_cacheLodRange.y() != levelRange.y()) {
|
||||
if (!isMultisampleTarget && m_cacheLodRange.y() != levelRange.y()) {
|
||||
g_GLESFuncs.glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, static_cast<GLint>(levelRange.y()));
|
||||
m_cacheLodRange.y() = levelRange.y();
|
||||
}
|
||||
@@ -2266,7 +2272,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
});
|
||||
}
|
||||
|
||||
if (m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
|
||||
if (!isMultisampleTarget && m_cacheBorderColor != stateTextureObject->GetBorderColor()) {
|
||||
const auto& borderColor = stateTextureObject->GetBorderColor();
|
||||
GLfloat borderColorArray[4] = {borderColor.x(), borderColor.y(), borderColor.z(), borderColor.w()};
|
||||
g_GLESFuncs.glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColorArray);
|
||||
|
||||
@@ -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,20 +2112,18 @@ 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; the env variable
|
||||
// forces the quirk on ("0") or off ("1") on any driver.
|
||||
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
|
||||
Bool suppressBlendedDepthWrite = m_physicalDevice.properties.vendorID == kVendorIdQualcomm;
|
||||
if (const char* env = getenv("MOBILEGL_MAGMA_BLENDED_DEPTH_WRITE")) {
|
||||
if (env[0] == '0') {
|
||||
suppressBlendedDepthWrite = true;
|
||||
} else if (env[0] == '1') {
|
||||
suppressBlendedDepthWrite = false;
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
const MG_Config::QuirkOverride quirkOverride =
|
||||
MG_Config::Features.MagmaDisableBlendedDepthWriteQuirk;
|
||||
const Bool suppressBlendedDepthWrite = PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(
|
||||
quirkOverride, m_physicalDevice.properties.vendorID);
|
||||
if (suppressBlendedDepthWrite) {
|
||||
MGLOG_I("DirectVulkan: suppressing depth writes on blended pipelines "
|
||||
"(driver lacks cross-pipeline position invariance)");
|
||||
MGLOG_I("DirectVulkan: suppressing depth writes on accumulation-blended pipelines "
|
||||
"(driver lacks cross-pipeline position invariance)%s",
|
||||
quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : "");
|
||||
}
|
||||
PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -295,7 +295,13 @@ DECLARE_GL_FUNCTION_HEAD(void, VertexAttribDivisor, GLuint index, GLuint divisor
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedback, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedback, target, id)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacks, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacks, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacks, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacks, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedback, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedback, id)
|
||||
// Transform feedback objects are not implemented, so no name is ever a live object. The shared
|
||||
// stub returns (type)1, telling a probing caller that every id it invents already exists; GL_FALSE
|
||||
// is both truthful and what the spec requires for a name that was never generated.
|
||||
MOBILEGL_GL_API GLboolean glIsTransformFeedback(GLuint id) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
return GL_FALSE;
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedback) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedback)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetProgramBinary, GLuint program, GLsizei bufSize, GLsizei* length, GLenum* binaryFormat, void* binary) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetProgramBinary, program, bufSize, length, binaryFormat, binary)
|
||||
@@ -2583,7 +2589,10 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, TransformFeedbackStreamAttribsNV, GLsizei co
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, BindTransformFeedbackNV, GLenum target, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, BindTransformFeedbackNV, target, id)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteTransformFeedbacksNV, GLsizei n, const GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteTransformFeedbacksNV, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, GenTransformFeedbacksNV, GLsizei n, GLuint* ids) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GenTransformFeedbacksNV, n, ids)
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(GLboolean, IsTransformFeedbackNV, GLuint id) DECLARE_GL_FUNCTION_STUB_END(GLboolean, IsTransformFeedbackNV, id)
|
||||
MOBILEGL_GL_API GLboolean glIsTransformFeedbackNV(GLuint id) {
|
||||
MGLOG_W("Stub function: %s(...)", __FUNCTION__);
|
||||
return GL_FALSE;
|
||||
}
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, PauseTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, PauseTransformFeedbackNV, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, ResumeTransformFeedbackNV, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, ResumeTransformFeedbackNV, )
|
||||
DECLARE_GL_FUNCTION_STUB_HEAD(void, DrawTransformFeedbackNV, GLenum mode, GLuint id) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DrawTransformFeedbackNV, mode, id)
|
||||
|
||||
@@ -350,6 +350,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
texture.AllocateStorage(uploadTarget, level, {levelTexelSize, levelByteSize});
|
||||
texture.MarkStorageDirty(uploadTarget, level, false);
|
||||
}
|
||||
// glGenerateMipmap defines exactly levels 0..requiredLevelCount-1. AllocateStorage only
|
||||
// grows, so a previously longer chain (a bigger base image before respecification) would
|
||||
// otherwise keep a tail of stale levels here and read as incomplete.
|
||||
texture.TruncateMipmapLevels(uploadTarget, requiredLevelCount);
|
||||
// Mip generation grows/regenerates the level set on the GPU without marking any CPU
|
||||
// level dirty (MarkStorageDirty(...,false) above). Bump the content version so the
|
||||
// backend re-syncs: a cached sampled VkImageView built for the pre-generate level
|
||||
@@ -429,8 +433,10 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
|
||||
const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat);
|
||||
if (samples > maxSamples) {
|
||||
// GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count
|
||||
// exceeds what the format supports, and the native Adreno driver agrees.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidValue,
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>(
|
||||
"MG_Impl/GLImpl", caller,
|
||||
std::format("Sample count {} exceeds the supported maximum {} for this texture format.",
|
||||
@@ -454,8 +460,56 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureObject->SetSamples(samples);
|
||||
textureObject->SetFixedSampleLocations(fixedsamplelocations == GL_TRUE);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, 0, {{width, height, depth}, 0});
|
||||
// Multisample textures are single-level by definition, so a name that previously held a
|
||||
// mip chain must not keep its tail now that AllocateStorage only grows.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, 1);
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, 0, false);
|
||||
}
|
||||
|
||||
// Redefining level 0 of a texture that already had a base image drops the rest of the chain,
|
||||
// which is exactly what AllocateLevel used to do implicitly for every level. Keeping that
|
||||
// behaviour for level 0 - and only for level 0 - is what makes the grow-only change safe:
|
||||
// any level-0 respecification leaves the chain in precisely the state it would have had
|
||||
// before, while an upload to level N no longer destroys the levels beneath it.
|
||||
//
|
||||
// Why it has to be *every* level-0 respecification and not just a size change: Minecraft's
|
||||
// Mipmap Levels setting rebuilds the block atlas at the SAME dimensions with a different
|
||||
// level count. A size-only test would leave the old tail in place, and because Mojang
|
||||
// terminates its chains with a 0x0 level the result is the zero-then-nonzero pattern that
|
||||
// IsComplete() rejects (TextureObject.cpp) - whereupon DirectGLES skips syncing the texture
|
||||
// entirely (Managers.cpp) and the atlas samples black.
|
||||
//
|
||||
// The "already has a base image" test is what lets the fix work at all: a level that was
|
||||
// never written reads back as {0,0,0}, so building a chain top-down - upload level N first,
|
||||
// then level 0 - must not discard the levels just uploaded. That ordering is what
|
||||
// KHR-GL33.texture_repeat_mode does.
|
||||
// Scoped to the respecified upload target only, which is what AllocateLevel already did.
|
||||
// Cube maps keep six independent chains while reporting a single level count (face +X), so
|
||||
// respecifying a face other than +X can leave the count longer than that face - but that
|
||||
// asymmetry predates this change and widening the truncation to all six faces would destroy
|
||||
// mip data for faces the application never touched. Left alone deliberately.
|
||||
void DiscardMipmapChainOnBaseRespecification(MG_State::GLState::TextureObjectMipmap* texture,
|
||||
TextureUploadTarget uploadTarget, Uint level) {
|
||||
if (level != 0) return;
|
||||
|
||||
const IntVec3 existingBaseSize = texture->GetMipmapTexelSize(uploadTarget, 0);
|
||||
const Bool hasExistingBaseImage =
|
||||
existingBaseSize.x() > 0 && existingBaseSize.y() > 0 && existingBaseSize.z() > 0;
|
||||
if (!hasExistingBaseImage) return;
|
||||
|
||||
texture->TruncateMipmapLevels(uploadTarget, 1);
|
||||
}
|
||||
|
||||
// Compressed texture upload is not implemented yet. GL_NUM_COMPRESSED_TEXTURE_FORMATS
|
||||
// reports 0, so every compressed internalformat is by definition unsupported and
|
||||
// GL_INVALID_ENUM is the specified error - unlike THROW_UNIMPL_EXCEPTION, which unwinds
|
||||
// a C++ exception through the C GL ABI and takes the process down.
|
||||
void RecordUnsupportedCompressedFormat(const char* caller) {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidEnum,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
|
||||
"Compressed texture formats are not supported."));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& GetTextureObjectByName(GLuint texture, const char* caller) {
|
||||
@@ -1727,6 +1781,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
if (isProxy) {
|
||||
MGLOG_D("%s: isProxy = true, not allocating", __func__);
|
||||
} else {
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, height, depth}, internalBytes});
|
||||
}
|
||||
|
||||
@@ -1854,6 +1909,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MGLOG_D("%s: isProxy = true, not allocating", __func__);
|
||||
} else {
|
||||
MGLOG_D("%s: Allocating %d bytes at mip %d", __func__, internalBytes, level);
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level,
|
||||
{{width, height, 1}, internalBytes});
|
||||
}
|
||||
@@ -1942,6 +1998,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
"Texture object here should always be an object with mipmap");
|
||||
auto textureMipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
if (!isProxy) {
|
||||
DiscardMipmapChainOnBaseRespecification(textureMipmapObject, textureUploadTarget, level);
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{width, 1, 1}, internalBytes});
|
||||
}
|
||||
|
||||
@@ -2594,7 +2651,13 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
}
|
||||
|
||||
void GetCompressedTexImage_State(GLenum target, GLint level, void* img) {
|
||||
// TODO: implement
|
||||
// TODO: implement compressed readback. Reporting success while writing nothing hands
|
||||
// the caller stale memory with GL_NO_ERROR; no texture can be compressed yet, and GL
|
||||
// specifies GL_INVALID_OPERATION when the bound level is not compressed.
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
|
||||
"Texture level is not stored in a compressed format."));
|
||||
}
|
||||
|
||||
void GenTextures_State(GLsizei n, GLuint* textures) {
|
||||
@@ -2781,20 +2844,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
void CompressedTexSubImage3D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset,
|
||||
GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize,
|
||||
const void* data) {
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexSubImage2D_State(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width,
|
||||
GLsizei height, GLenum format, GLsizei imageSize, const void* data) {
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexSubImage1D_State(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format,
|
||||
GLsizei imageSize, const void* data) {
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexImage3D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
@@ -2804,8 +2867,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexImage2D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height,
|
||||
@@ -2815,8 +2878,11 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload. Until then report the spec error for an
|
||||
// unsupported compressed format rather than throwing - a C++ exception unwinding
|
||||
// through the C GL ABI is a hard crash for the caller, while GL_INVALID_ENUM is
|
||||
// exactly what GL_NUM_COMPRESSED_TEXTURE_FORMATS == 0 promises.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void CompressedTexImage1D_State(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLint border,
|
||||
@@ -2826,8 +2892,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& textureObject = GetTextureObjectByTarget(textureUploadTarget, textureTarget);
|
||||
if (!ValidateTextureMutable(textureObject, __func__)) return;
|
||||
|
||||
// TODO: implement
|
||||
THROW_UNIMPL_EXCEPTION;
|
||||
// TODO: implement compressed upload - see CompressedTexImage2D_State.
|
||||
RecordUnsupportedCompressedFormat(__func__);
|
||||
}
|
||||
|
||||
void BindTexture_State(GLenum target, GLuint texture) {
|
||||
@@ -3173,6 +3239,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, 1, 1}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||
}
|
||||
// Immutable storage defines exactly `levels` levels; AllocateStorage only grows, so a
|
||||
// longer pre-existing chain has to be dropped explicitly.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
@@ -3225,6 +3294,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
textureMipmapObject->AllocateStorage(textureUploadTarget, level, {{levelWidth, levelHeight, 1}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||
}
|
||||
// See TextureStorage1D.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
@@ -3277,6 +3348,8 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
{{levelWidth, levelHeight, levelDepth}, byteSize});
|
||||
textureMipmapObject->MarkStorageDirty(textureUploadTarget, level, false);
|
||||
}
|
||||
// See TextureStorage1D.
|
||||
textureMipmapObject->TruncateMipmapLevels(textureUploadTarget, static_cast<Uint>(levels));
|
||||
textureObject->SetImmutableLevels(static_cast<Uint>(levels));
|
||||
}
|
||||
|
||||
|
||||
@@ -16,17 +16,32 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
void MipmapStorage::AllocateLevel(Uint level, MipmapInput input) {
|
||||
m_data.reserve(std::bit_ceil(level + 1));
|
||||
m_data.resize(level + 1);
|
||||
m_texelSizes.reserve(std::bit_ceil(level + 1));
|
||||
m_texelSizes.resize(level + 1);
|
||||
m_texelSizes[level] = input.texelSize;
|
||||
m_isDirty.resize(level + 1, false);
|
||||
// Grow only. GL respecifies exactly the level it is handed, so allocating level 0
|
||||
// must not disturb the levels above it - but resize() shrinks as readily as it
|
||||
// grows, so this used to truncate the whole chain to a single level. Callers that
|
||||
// genuinely redefine the complete level set say so with TruncateToLevelCount.
|
||||
const SizeT requiredLevelCount = static_cast<SizeT>(level) + 1;
|
||||
if (m_data.size() < requiredLevelCount) {
|
||||
m_data.reserve(std::bit_ceil(requiredLevelCount));
|
||||
m_data.resize(requiredLevelCount);
|
||||
m_texelSizes.reserve(std::bit_ceil(requiredLevelCount));
|
||||
m_texelSizes.resize(requiredLevelCount);
|
||||
m_isDirty.resize(requiredLevelCount, false);
|
||||
}
|
||||
|
||||
m_texelSizes[level] = input.texelSize;
|
||||
auto& data = m_data[level];
|
||||
data.resize(input.byteSize, 0);
|
||||
}
|
||||
|
||||
void MipmapStorage::TruncateToLevelCount(SizeT levelCount) {
|
||||
if (levelCount >= m_data.size()) return;
|
||||
|
||||
m_data.resize(levelCount);
|
||||
m_texelSizes.resize(levelCount);
|
||||
m_isDirty.resize(levelCount);
|
||||
}
|
||||
|
||||
void MipmapStorage::UpdateSubData(Uint level, DataPtr input) {
|
||||
auto& targetData = m_data;
|
||||
MOBILEGL_ASSERT(level < targetData.size(), "UpdateSubData: level out of range");
|
||||
@@ -55,6 +70,7 @@ namespace MobileGL {
|
||||
}
|
||||
|
||||
SizeT MipmapStorage::GetByteSize(Uint level) const {
|
||||
if (level >= m_data.size()) return 0;
|
||||
return m_data[level].size();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ namespace MobileGL {
|
||||
public:
|
||||
SizeT GetLevelCount() const;
|
||||
void AllocateLevel(Uint level, MipmapInput input);
|
||||
// Discard every level at or above levelCount. AllocateLevel never shrinks, so this
|
||||
// is the only way a chain gets shorter - use it where the caller defines the whole
|
||||
// level set (glTexStorage*, mip regeneration, atlas respecification).
|
||||
void TruncateToLevelCount(SizeT levelCount);
|
||||
void UpdateSubData(Uint level, DataPtr input);
|
||||
void* MapData(Uint level);
|
||||
IntVec3 GetTexelSize(Uint level) const;
|
||||
|
||||
@@ -29,6 +29,14 @@ namespace MobileGL {
|
||||
m_storage[targetIndex].AllocateLevel(level, input);
|
||||
}
|
||||
|
||||
// Per-target, like AllocateLevel: cube-map faces are respecified independently, so
|
||||
// truncating one face must not disturb the others.
|
||||
void TruncateToLevelCount(Uint targetIndex, SizeT levelCount) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "TruncateToLevelCount: target invalid");
|
||||
|
||||
m_storage[targetIndex].TruncateToLevelCount(levelCount);
|
||||
}
|
||||
|
||||
void UpdateSubData(Uint targetIndex, Uint level, DataPtr input) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "UpdateSubData: target invalid");
|
||||
m_storage[targetIndex].UpdateSubData(level, input);
|
||||
|
||||
@@ -271,6 +271,10 @@ namespace MobileGL {
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
DataPtr input) {
|
||||
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
|
||||
@@ -134,6 +134,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
virtual const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
|
||||
virtual const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const = 0;
|
||||
virtual void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) = 0;
|
||||
// AllocateStorage only ever grows the chain. Callers that define the complete level set -
|
||||
// glTexStorage*, mip regeneration, or a level-0 respecification at a new size - drop the
|
||||
// leftovers explicitly, so a stale tail can never make the texture silently incomplete.
|
||||
virtual void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) = 0;
|
||||
virtual void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) = 0;
|
||||
virtual void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) = 0;
|
||||
virtual void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty = true) = 0;
|
||||
@@ -175,6 +179,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
|
||||
void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
|
||||
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) override;
|
||||
|
||||
@@ -31,6 +31,10 @@ namespace MobileGL {
|
||||
m_textureStorage.AllocateLevel(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) {
|
||||
m_textureStorage.TruncateToLevelCount(GetIndexOfTextureUploadTarget(uploadTarget), levelCount);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
DataPtr input) {
|
||||
m_textureStorage.UpdateSubData(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, input);
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace MobileGL {
|
||||
const IntVec3 GetMipmapTexelSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
const SizeT GetMipmapByteSize(TextureUploadTarget target, Uint mipmapLevel) const override;
|
||||
void AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) override;
|
||||
void TruncateMipmapLevels(TextureUploadTarget uploadTarget, Uint levelCount) override;
|
||||
void UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) override;
|
||||
void* MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) override;
|
||||
void MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, bool dirty) override;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -1427,6 +1427,95 @@ TEST_F(TextureTest, TextureStorage1DAndSubImageModifyNamedObjectOnly) {
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Building a mip chain top-down - upload level N, then level 0 - must not destroy the levels
|
||||
// already uploaded. AllocateLevel used to resize() the storage down to level+1 on every call, so
|
||||
// the level-0 upload truncated the chain to a single level; the higher level then read back as
|
||||
// {0,0,0}, IsComplete() rejected the zero-then-nonzero pattern, and DirectGLES answered that by
|
||||
// skipping the texture's sync entirely. This is the shape KHR-GL33.texture_repeat_mode uses, and
|
||||
// it accounted for 108 CTS failures in every GL version.
|
||||
TEST_F(TextureTest, TexImage2DOnLevelZeroKeepsAnAlreadyUploadedHigherLevel) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 49, 23, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 98, 46, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
ASSERT_NE(mipmapObject, nullptr);
|
||||
EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 2u);
|
||||
EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0), IntVec3(98, 46, 1));
|
||||
EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 1), IntVec3(49, 23, 1));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// The other half of the contract: respecifying a level 0 that already held an image still drops
|
||||
// the chain, exactly as before. Minecraft rebinds the block-atlas name and calls glTexImage2D on
|
||||
// level 0 before uploading the new levels; leaving the previous chain in place would strand a tail
|
||||
// at the wrong sizes and - because Mojang terminates its chains with a 0x0 level - reproduce the
|
||||
// same incomplete-texture black atlas the fix above exists to prevent.
|
||||
TEST_F(TextureTest, TexImage2DRespecifyingAnExistingLevelZeroDropsTheStaleChain) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
ASSERT_NE(mipmapObject, nullptr);
|
||||
ASSERT_EQ(mipmapObject->GetMipmapLevelCount(), 3u);
|
||||
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
|
||||
EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 1u);
|
||||
EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0), IntVec3(16, 16, 1));
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Same-size respecification has to drop the chain too. The Mipmap Levels video setting rebuilds
|
||||
// the atlas at identical dimensions with a different level count, so a size-change-only test would
|
||||
// let the old tail survive.
|
||||
TEST_F(TextureTest, TexImage2DRespecifyingLevelZeroAtTheSameSizeStillDropsTheChain) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
ASSERT_NE(mipmapObject, nullptr);
|
||||
EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 1u);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// glTexStorage2D defines exactly `levels` levels. AllocateStorage only grows now, so the immutable
|
||||
// path has to drop a longer pre-existing chain explicitly.
|
||||
TEST_F(TextureTest, TexStorage2DTrimsALongerPreExistingMipChain) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::GenTextures(1, &texture);
|
||||
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
|
||||
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 3, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
|
||||
MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 8, 8);
|
||||
|
||||
const auto textureObject = MG_State::pGLContext->GetTextureObject(texture);
|
||||
auto* mipmapObject = static_cast<MG_State::GLState::TextureObjectMipmap*>(textureObject.get());
|
||||
ASSERT_NE(mipmapObject, nullptr);
|
||||
EXPECT_EQ(mipmapObject->GetMipmapLevelCount(), 2u);
|
||||
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
TEST_F(TextureTest, TextureStorage3DAndSubImageModifyNamedObjectOnly) {
|
||||
GLuint texture = 0;
|
||||
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture);
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user