// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.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 "ProgramFactory.h" #include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h" #include "MG_Util/ShaderTranspiler/ShaderCompiler.h" #include "MG_Util/ShaderTranspiler/SpvcSession.h" #include "MG_Util/ShaderTranspiler/Types.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace MobileGL::MG_Backend::DirectVulkan { namespace { using ShaderObject = MG_State::GLState::ShaderObject; using SpvcSession = MG_Util::ShaderTranspiler::SpvcSession; using SessionUsageBit = MG_Util::ShaderTranspiler::SessionUsageBit; struct DescriptorKey { ProgramFactory::DescriptorBindingKind kind = ProgramFactory::DescriptorBindingKind::None; String name; Bool operator==(const DescriptorKey& other) const { return kind == other.kind && name == other.name; } }; struct DescriptorKeyHash { SizeT operator()(const DescriptorKey& key) const noexcept { return std::hash{}(key.name) ^ (static_cast(key.kind) << 1); } }; struct PositionTargetInfo { Uint32 variableId = 0; Uint32 vectorTypeId = 0; Uint32 floatTypeId = 0; Uint32 vectorPtrTypeId = 0; Uint32 memberIndex = 0; Bool isMember = false; }; ShaderStage PickClipFixupStage(const Vector>& shaders); Bool IsVec4Float32(spvtools::opt::IRContext* context, Uint32 typeId, Uint32* outFloatTypeId) { auto* vecInst = context->get_def_use_mgr()->GetDef(typeId); if (!vecInst || vecInst->opcode() != spv::Op::OpTypeVector) return false; if (vecInst->GetSingleWordInOperand(1) != 4) return false; const Uint32 floatTypeId = vecInst->GetSingleWordInOperand(0); auto* floatInst = context->get_def_use_mgr()->GetDef(floatTypeId); if (!floatInst || floatInst->opcode() != spv::Op::OpTypeFloat) return false; if (floatInst->GetSingleWordInOperand(0) != 32) return false; if (outFloatTypeId) *outFloatTypeId = floatTypeId; return true; } spvc_basetype MapReflectInterfaceToSpvcBasetype(const SpvReflectInterfaceVariable& variable) { if (variable.type_description == nullptr) { return SPVC_BASETYPE_UNKNOWN; } const auto flags = variable.type_description->type_flags; const auto width = variable.numeric.scalar.width; const auto signedness = variable.numeric.scalar.signedness; if ((flags & SPV_REFLECT_TYPE_FLAG_FLOAT) != 0) { switch (width) { case 16: return SPVC_BASETYPE_FP16; case 32: return SPVC_BASETYPE_FP32; case 64: return SPVC_BASETYPE_FP64; default: return SPVC_BASETYPE_UNKNOWN; } } if ((flags & SPV_REFLECT_TYPE_FLAG_INT) != 0) { if (signedness != 0) { switch (width) { case 8: return SPVC_BASETYPE_INT8; case 16: return SPVC_BASETYPE_INT16; case 32: return SPVC_BASETYPE_INT32; case 64: return SPVC_BASETYPE_INT64; default: return SPVC_BASETYPE_UNKNOWN; } } switch (width) { case 8: return SPVC_BASETYPE_UINT8; case 16: return SPVC_BASETYPE_UINT16; case 32: return SPVC_BASETYPE_UINT32; case 64: return SPVC_BASETYPE_UINT64; default: return SPVC_BASETYPE_UNKNOWN; } } if ((flags & SPV_REFLECT_TYPE_FLAG_BOOL) != 0) { return SPVC_BASETYPE_BOOLEAN; } return SPVC_BASETYPE_UNKNOWN; } Uint32 GetReflectInterfaceLocationSpan(const SpvReflectInterfaceVariable& variable) { Uint32 locationSpan = variable.numeric.matrix.column_count; if (locationSpan == 0) { locationSpan = 1; } for (Uint32 dimIndex = 0; dimIndex < variable.array.dims_count; ++dimIndex) { const Uint32 dim = variable.array.dims[dimIndex]; if (dim == 0 || dim == SPV_REFLECT_ARRAY_DIM_RUNTIME) { continue; } locationSpan *= dim; } return locationSpan; } GLenum GetReflectInterfaceLocationType(const SpvReflectInterfaceVariable& variable) { MG_Util::ShaderTranspiler::SpvcType spvcType{}; spvcType.basetype = MapReflectInterfaceToSpvcBasetype(variable); spvcType.vectorSize = variable.numeric.vector.component_count; if (spvcType.vectorSize == 0) { spvcType.vectorSize = variable.numeric.matrix.row_count; } if (spvcType.vectorSize == 0) { spvcType.vectorSize = 1; } spvcType.matCol = 1; if (spvcType.vectorSize < 1 || spvcType.vectorSize > 4) { return GL_FALSE; } switch (spvcType.basetype) { case SPVC_BASETYPE_BOOLEAN: switch (spvcType.vectorSize) { case 1: return GL_BOOL; case 2: return GL_BOOL_VEC2; case 3: return GL_BOOL_VEC3; case 4: return GL_BOOL_VEC4; default: return GL_FALSE; } case SPVC_BASETYPE_INT32: switch (spvcType.vectorSize) { case 1: return GL_INT; case 2: return GL_INT_VEC2; case 3: return GL_INT_VEC3; case 4: return GL_INT_VEC4; default: return GL_FALSE; } case SPVC_BASETYPE_UINT32: switch (spvcType.vectorSize) { case 1: return GL_UNSIGNED_INT; case 2: return GL_UNSIGNED_INT_VEC2; case 3: return GL_UNSIGNED_INT_VEC3; case 4: return GL_UNSIGNED_INT_VEC4; default: return GL_FALSE; } case SPVC_BASETYPE_FP32: switch (spvcType.vectorSize) { case 1: return GL_FLOAT; case 2: return GL_FLOAT_VEC2; case 3: return GL_FLOAT_VEC3; case 4: return GL_FLOAT_VEC4; default: return GL_FALSE; } case SPVC_BASETYPE_FP64: switch (spvcType.vectorSize) { case 1: return GL_DOUBLE; case 2: return GL_DOUBLE_VEC2; case 3: return GL_DOUBLE_VEC3; case 4: return GL_DOUBLE_VEC4; default: return GL_FALSE; } default: return GL_FALSE; } } Uint32 GetReflectInterfaceLocationSignature(const SpvReflectInterfaceVariable& variable) { Uint32 vectorSize = variable.numeric.vector.component_count; if (vectorSize == 0) { vectorSize = variable.numeric.matrix.row_count; } if (vectorSize == 0) { vectorSize = 1; } if (vectorSize < 1 || vectorSize > 4) { return 0; } Uint32 typeClass = 0; Uint32 scalarWidth = 0; switch (MapReflectInterfaceToSpvcBasetype(variable)) { case SPVC_BASETYPE_BOOLEAN: typeClass = 1; scalarWidth = 1; break; case SPVC_BASETYPE_INT8: typeClass = 2; scalarWidth = 8; break; case SPVC_BASETYPE_INT16: typeClass = 2; scalarWidth = 16; break; case SPVC_BASETYPE_INT32: typeClass = 2; scalarWidth = 32; break; case SPVC_BASETYPE_INT64: typeClass = 2; scalarWidth = 64; break; case SPVC_BASETYPE_UINT8: typeClass = 3; scalarWidth = 8; break; case SPVC_BASETYPE_UINT16: typeClass = 3; scalarWidth = 16; break; case SPVC_BASETYPE_UINT32: typeClass = 3; scalarWidth = 32; break; case SPVC_BASETYPE_UINT64: typeClass = 3; scalarWidth = 64; break; case SPVC_BASETYPE_FP16: typeClass = 4; scalarWidth = 16; break; case SPVC_BASETYPE_FP32: typeClass = 4; scalarWidth = 32; break; case SPVC_BASETYPE_FP64: typeClass = 4; scalarWidth = 64; break; default: return 0; } return (typeClass << 24) | (scalarWidth << 8) | vectorSize; } Uint32 GetReflectInterfaceVectorSize(const SpvReflectInterfaceVariable& variable) { Uint32 vectorSize = variable.numeric.vector.component_count; if (vectorSize == 0) { vectorSize = variable.numeric.matrix.row_count; } if (vectorSize == 0) { vectorSize = 1; } return vectorSize; } struct StageInterfaceCursor { Uint32 location = 0; Uint32 component = 0; }; struct StageInterfaceSummary { static constexpr Uint32 kMaxComponentSlots = ProgramFactory::VkProgramObject::kMaxVertexInputLocations * 4; Array slotSignatures{}; Array slotDebugNames{}; }; Uint32 CountOccupiedStageInterfaceSlots(const StageInterfaceSummary& summary) { Uint32 occupiedSlotCount = 0; for (Uint32 slotIndex = 0; slotIndex < StageInterfaceSummary::kMaxComponentSlots; ++slotIndex) { if (summary.slotSignatures[slotIndex] != 0) { ++occupiedSlotCount; } } return occupiedSlotCount; } spv_target_env GetSpirvTargetEnv(const Vector& spirv) { spv_target_env targetEnv = SPV_ENV_VULKAN_1_0; if (spirv.size() > 1) { const Uint32 versionWord = spirv[1]; const Uint32 major = (versionWord >> 16) & 0xffu; const Uint32 minor = (versionWord >> 8) & 0xffu; if (major > 1 || (major == 1 && minor >= 6)) { targetEnv = SPV_ENV_VULKAN_1_3; } else if (major == 1 && minor >= 5) { targetEnv = SPV_ENV_VULKAN_1_2; } else if (major == 1 && minor >= 4) { targetEnv = SPV_ENV_VULKAN_1_1_SPIRV_1_4; } else if (major == 1 && minor >= 3) { targetEnv = SPV_ENV_VULKAN_1_1; } } return targetEnv; } Bool IsInterfaceVariableStaticallyUsed(const Vector& spirv, Uint32 spirvId) { if (spirv.empty() || spirvId == 0) { return false; } auto context = spvtools::BuildModule( GetSpirvTargetEnv(spirv), [](spv_message_level_t, const char*, const spv_position_t&, const char*) {}, spirv.data(), spirv.size()); if (!context) { return true; } auto* variable = context->get_def_use_mgr()->GetDef(spirvId); if (variable == nullptr) { return false; } Bool used = false; context->get_def_use_mgr()->ForEachUser(variable, [&used](spvtools::opt::Instruction* user) { switch (user->opcode()) { case spv::Op::OpName: case spv::Op::OpMemberName: case spv::Op::OpDecorate: case spv::Op::OpMemberDecorate: case spv::Op::OpDecorateId: case spv::Op::OpEntryPoint: return; default: used = true; return; } }); return used; } void ValidateTransformedSpirv(const Vector& spirv, ShaderStage shaderStage, Uint programExternalIndex) { if (spirv.empty()) { return; } spv_const_binary_t binary = {spirv.data(), spirv.size()}; const spv_target_env targetEnv = GetSpirvTargetEnv(spirv); spv_context context = spvContextCreate(targetEnv); MOBILEGL_ASSERT(context != nullptr, "ProgramFactory::ValidateTransformedSpirv: failed to create validator context for stage=%d program=%u", static_cast(shaderStage), programExternalIndex); spv_validator_options options = spvValidatorOptionsCreate(); MOBILEGL_ASSERT(options != nullptr, "ProgramFactory::ValidateTransformedSpirv: failed to create validator options for stage=%d program=%u", static_cast(shaderStage), programExternalIndex); spvValidatorOptionsSetFriendlyNames(options, true); spv_diagnostic diagnostic = nullptr; const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic); if (result != SPV_SUCCESS) { // MGLOG_E, unlatched: reaching here already requires the validation switch to // be armed, which bounds the volume, and each VUID names a different defect. // (Parked at MGLOG_I until the Log.h level ordering was fixed, when E was // compiled out of every INFO build.) The latch is what a test harness asserts on. MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure(); MGLOG_E( "ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d index=%zu msg=%s", static_cast(shaderStage), programExternalIndex, static_cast(result), diagnostic != nullptr ? diagnostic->position.index : 0, diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : ""); } MOBILEGL_ASSERT( result == SPV_SUCCESS, "ProgramFactory::ValidateTransformedSpirv: validation failed for stage=%d program=%u result=%d line=%zu column=%zu index=%zu msg=%s", static_cast(shaderStage), programExternalIndex, static_cast(result), diagnostic != nullptr ? diagnostic->position.line : 0, diagnostic != nullptr ? diagnostic->position.column : 0, diagnostic != nullptr ? diagnostic->position.index : 0, diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : ""); spvDiagnosticDestroy(diagnostic); spvValidatorOptionsDestroy(options); spvContextDestroy(context); } void ReflectStageInterfaceVariable(const SpvReflectInterfaceVariable& variable, Bool reflectInputs, StageInterfaceSummary& outSummary, Uint programExternalIndex, const char* stageLabel, StageInterfaceCursor& cursor, Uint32 locationBase = 0, Bool allowImplicitPacking = false, const char* inheritedName = nullptr) { if ((variable.decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0) { return; } const char* debugName = variable.name; if (debugName == nullptr || debugName[0] == '\0') { debugName = inheritedName; } if (debugName == nullptr || debugName[0] == '\0') { debugName = ""; } const Bool hasConcreteLocation = allowImplicitPacking ? (variable.location != 0 || variable.component != 0) : variable.location != std::numeric_limits::max(); const Bool hasConcreteComponent = allowImplicitPacking ? (variable.component != 0) : variable.component != std::numeric_limits::max(); const Uint32 explicitLocationBase = locationBase + (hasConcreteLocation ? variable.location : 0u); if (variable.member_count > 0 && variable.members != nullptr) { StageInterfaceCursor memberCursor = cursor; if (hasConcreteLocation) { memberCursor.location = explicitLocationBase; memberCursor.component = 0; } for (Uint32 memberIndex = 0; memberIndex < variable.member_count; ++memberIndex) { ReflectStageInterfaceVariable(variable.members[memberIndex], reflectInputs, outSummary, programExternalIndex, stageLabel, memberCursor, explicitLocationBase, true, debugName); } if (memberCursor.location > cursor.location || (memberCursor.location == cursor.location && memberCursor.component > cursor.component)) { cursor = memberCursor; } return; } MOBILEGL_ASSERT( hasConcreteLocation || allowImplicitPacking || locationBase != 0, "ProgramFactory::ReflectStageInterface: missing concrete %s %s location for name='%s' program=%u", stageLabel, reflectInputs ? "input" : "output", debugName, programExternalIndex); const Uint32 component = variable.component; MOBILEGL_ASSERT( component < 4 || component == std::numeric_limits::max(), "ProgramFactory::ReflectStageInterface: unsupported %s %s component=%u at location=%u name='%s' program=%u", stageLabel, reflectInputs ? "input" : "output", component, explicitLocationBase, debugName, programExternalIndex); const Uint32 locationSignature = GetReflectInterfaceLocationSignature(variable); MOBILEGL_ASSERT( locationSignature != 0, "ProgramFactory::ReflectStageInterface: unsupported %s %s type at location=%u name='%s' flags=0x%x width=%u signed=%u vec=%u rows=%u cols=%u program=%u", stageLabel, reflectInputs ? "input" : "output", explicitLocationBase, debugName, static_cast(variable.type_description != nullptr ? variable.type_description->type_flags : 0), variable.numeric.scalar.width, variable.numeric.scalar.signedness, variable.numeric.vector.component_count, variable.numeric.matrix.row_count, variable.numeric.matrix.column_count, programExternalIndex); const Uint32 vectorSize = GetReflectInterfaceVectorSize(variable); const Uint32 locationSpan = GetReflectInterfaceLocationSpan(variable); Uint32 startLocation = explicitLocationBase; Uint32 startComponent = hasConcreteComponent ? component : 0u; const Bool useImplicitPacking = allowImplicitPacking && !hasConcreteLocation && !hasConcreteComponent; if (useImplicitPacking) { startLocation = cursor.location; startComponent = cursor.component; if (locationSpan > 1 || startComponent + vectorSize > 4) { if (startComponent != 0) { ++startLocation; startComponent = 0; } if (locationSpan == 1 && startComponent + vectorSize > 4) { ++startLocation; startComponent = 0; } } } MOBILEGL_ASSERT( startComponent < 4, "ProgramFactory::ReflectStageInterface: %s %s component overflow at location=%u component=%u name='%s' program=%u", stageLabel, reflectInputs ? "input" : "output", startLocation, startComponent, debugName, programExternalIndex); MOBILEGL_ASSERT( locationSpan == 1 || startComponent == 0, "ProgramFactory::ReflectStageInterface: %s %s multi-location variable starts at non-zero component location=%u component=%u name='%s' program=%u", stageLabel, reflectInputs ? "input" : "output", startLocation, startComponent, debugName, programExternalIndex); for (Uint32 locationOffset = 0; locationOffset < locationSpan; ++locationOffset) { const Uint32 expandedLocation = startLocation + locationOffset; const Uint32 componentBase = (locationOffset == 0) ? startComponent : 0u; MOBILEGL_ASSERT( expandedLocation < ProgramFactory::VkProgramObject::kMaxVertexInputLocations, "ProgramFactory::ReflectStageInterface: %s %s location=%u span=%u exceeds tracked limit for name='%s' program=%u", stageLabel, reflectInputs ? "input" : "output", startLocation, locationSpan, debugName, programExternalIndex); MOBILEGL_ASSERT( componentBase + vectorSize <= 4, "ProgramFactory::ReflectStageInterface: %s %s component span overflow at location=%u component=%u vec=%u name='%s' program=%u", stageLabel, reflectInputs ? "input" : "output", expandedLocation, componentBase, vectorSize, debugName, programExternalIndex); for (Uint32 componentOffset = 0; componentOffset < vectorSize; ++componentOffset) { const Uint32 expandedComponent = componentBase + componentOffset; const Uint32 slotIndex = expandedLocation * 4 + expandedComponent; MOBILEGL_ASSERT( outSummary.slotSignatures[slotIndex] == 0 || outSummary.slotSignatures[slotIndex] == locationSignature, "ProgramFactory::ReflectStageInterface: conflicting %s %s type at location=%u component=%u existingSignature=0x%x existingName='%s' newSignature=0x%x newName='%s' program=%u", stageLabel, reflectInputs ? "input" : "output", expandedLocation, expandedComponent, outSummary.slotSignatures[slotIndex], outSummary.slotDebugNames[slotIndex].empty() ? "" : outSummary.slotDebugNames[slotIndex].c_str(), locationSignature, debugName, programExternalIndex); outSummary.slotSignatures[slotIndex] = locationSignature; outSummary.slotDebugNames[slotIndex] = debugName; } } StageInterfaceCursor endCursor{}; if (locationSpan > 1) { endCursor.location = startLocation + locationSpan; endCursor.component = 0; } else { endCursor.location = startLocation; endCursor.component = startComponent + vectorSize; if (endCursor.component >= 4) { endCursor.location += endCursor.component / 4; endCursor.component %= 4; } } if (endCursor.location > cursor.location || (endCursor.location == cursor.location && endCursor.component > cursor.component)) { cursor = endCursor; } } void ReflectStageInterface(ShaderStage targetStage, Bool reflectInputs, const Vector>& shaders, const Vector>& spirv, StageInterfaceSummary& outSummary, Uint programExternalIndex, const char* stageLabel) { outSummary.slotSignatures.fill(0); for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) { if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != targetStage) { continue; } const auto& module = spirv[moduleIndex]; if (module.empty()) { continue; } SpvReflectShaderModule reflectModule{}; const SpvReflectResult createResult = spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule); MOBILEGL_ASSERT( createResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectStageInterface: failed to create reflection module for %s %s (result=%d program=%u)", stageLabel, reflectInputs ? "input" : "output", static_cast(createResult), programExternalIndex); if (createResult != SPV_REFLECT_RESULT_SUCCESS) { continue; } uint32_t variableCount = 0; SpvReflectResult reflectResult = reflectInputs ? spvReflectEnumerateInputVariables(&reflectModule, &variableCount, nullptr) : spvReflectEnumerateOutputVariables(&reflectModule, &variableCount, nullptr); MOBILEGL_ASSERT( reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectStageInterface: failed to enumerate %s %s variables (result=%d program=%u)", stageLabel, reflectInputs ? "input" : "output", static_cast(reflectResult), programExternalIndex); Vector variables(variableCount); if (reflectResult == SPV_REFLECT_RESULT_SUCCESS && variableCount > 0) { reflectResult = reflectInputs ? spvReflectEnumerateInputVariables(&reflectModule, &variableCount, variables.data()) : spvReflectEnumerateOutputVariables(&reflectModule, &variableCount, variables.data()); MOBILEGL_ASSERT( reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectStageInterface: failed to fetch %s %s variables (result=%d program=%u)", stageLabel, reflectInputs ? "input" : "output", static_cast(reflectResult), programExternalIndex); } if (reflectResult == SPV_REFLECT_RESULT_SUCCESS) { StageInterfaceCursor stageCursor{}; for (auto* variable : variables) { if (variable == nullptr) { continue; } if (reflectInputs && !IsInterfaceVariableStaticallyUsed(module, variable->spirv_id)) { continue; } ReflectStageInterfaceVariable(*variable, reflectInputs, outSummary, programExternalIndex, stageLabel, stageCursor); } } spvReflectDestroyShaderModule(&reflectModule); break; } } void ValidateRasterizationStageInterface(const Vector>& shaders, const Vector>& spirv, ProgramFactory::VkProgramObject& entry, Uint programExternalIndex) { const ShaderStage producerStage = PickClipFixupStage(shaders); entry.rasterizationProducerStage = producerStage; entry.producerOutputComponentCount = 0; entry.fragmentInputComponentCount = 0; if (producerStage == ShaderStage::Unknown) { return; } Bool hasFragmentStage = false; for (const auto& shader : shaders) { if (shader && shader->GetShaderStage() == ShaderStage::Fragment) { hasFragmentStage = true; break; } } if (!hasFragmentStage) { return; } StageInterfaceSummary producerOutputs{}; StageInterfaceSummary fragmentInputs{}; ReflectStageInterface(producerStage, false, shaders, spirv, producerOutputs, programExternalIndex, "producer"); ReflectStageInterface(ShaderStage::Fragment, true, shaders, spirv, fragmentInputs, programExternalIndex, "fragment"); entry.producerOutputComponentCount = CountOccupiedStageInterfaceSlots(producerOutputs); entry.fragmentInputComponentCount = CountOccupiedStageInterfaceSlots(fragmentInputs); for (Uint32 slotIndex = 0; slotIndex < StageInterfaceSummary::kMaxComponentSlots; ++slotIndex) { if (fragmentInputs.slotSignatures[slotIndex] == 0) { continue; } MOBILEGL_ASSERT( producerOutputs.slotSignatures[slotIndex] == fragmentInputs.slotSignatures[slotIndex], "ProgramFactory::ValidateRasterizationStageInterface: location=%u component=%u producerSignature=0x%x producerName='%s' fragmentSignature=0x%x fragmentName='%s' program=%u", slotIndex / 4, slotIndex % 4, producerOutputs.slotSignatures[slotIndex], producerOutputs.slotDebugNames[slotIndex].empty() ? "" : producerOutputs.slotDebugNames[slotIndex].c_str(), fragmentInputs.slotSignatures[slotIndex], fragmentInputs.slotDebugNames[slotIndex].empty() ? "" : fragmentInputs.slotDebugNames[slotIndex].c_str(), programExternalIndex); } } Bool ResolveDirectPositionTarget(spvtools::opt::IRContext* context, Uint32 variableId, PositionTargetInfo* outTarget) { auto* varInst = context->get_def_use_mgr()->GetDef(variableId); if (!varInst || varInst->opcode() != spv::Op::OpVariable) return false; if (varInst->GetSingleWordInOperand(0) != static_cast(spv::StorageClass::Output)) return false; auto* ptrTypeInst = context->get_def_use_mgr()->GetDef(varInst->type_id()); if (!ptrTypeInst || ptrTypeInst->opcode() != spv::Op::OpTypePointer) return false; if (ptrTypeInst->GetSingleWordInOperand(0) != static_cast(spv::StorageClass::Output)) return false; PositionTargetInfo target{}; target.variableId = variableId; target.vectorTypeId = ptrTypeInst->GetSingleWordInOperand(1); if (!IsVec4Float32(context, target.vectorTypeId, &target.floatTypeId)) return false; target.vectorPtrTypeId = varInst->type_id(); target.isMember = false; *outTarget = target; return true; } Uint32 FindOutputVectorPointerTypeId(spvtools::opt::IRContext* context, Uint32 vectorTypeId) { auto* vectorType = context->get_type_mgr()->GetType(vectorTypeId); if (!vectorType) return 0; spvtools::opt::analysis::Pointer ptrType(vectorType, spv::StorageClass::Output); return context->get_type_mgr()->GetTypeInstruction(&ptrType); } Bool ResolveMemberPositionTarget(spvtools::opt::IRContext* context, Uint32 structTypeId, Uint32 memberIndex, PositionTargetInfo* outTarget) { auto* structInst = context->get_def_use_mgr()->GetDef(structTypeId); if (!structInst || structInst->opcode() != spv::Op::OpTypeStruct) return false; if (memberIndex >= structInst->NumInOperands()) return false; const Uint32 vectorTypeId = structInst->GetSingleWordInOperand(memberIndex); Uint32 floatTypeId = 0; if (!IsVec4Float32(context, vectorTypeId, &floatTypeId)) return false; const Uint32 vectorPtrTypeId = FindOutputVectorPointerTypeId(context, vectorTypeId); if (vectorPtrTypeId == 0) return false; for (auto& inst : context->module()->types_values()) { if (inst.opcode() != spv::Op::OpVariable) continue; if (inst.GetSingleWordInOperand(0) != static_cast(spv::StorageClass::Output)) continue; auto* ptrTypeInst = context->get_def_use_mgr()->GetDef(inst.type_id()); if (!ptrTypeInst || ptrTypeInst->opcode() != spv::Op::OpTypePointer) continue; if (ptrTypeInst->GetSingleWordInOperand(0) != static_cast(spv::StorageClass::Output)) continue; if (ptrTypeInst->GetSingleWordInOperand(1) != structTypeId) continue; PositionTargetInfo target{}; target.variableId = inst.result_id(); target.vectorTypeId = vectorTypeId; target.floatTypeId = floatTypeId; target.vectorPtrTypeId = vectorPtrTypeId; target.memberIndex = memberIndex; target.isMember = true; *outTarget = target; return true; } return false; } Bool FindPositionTarget(spvtools::opt::IRContext* context, PositionTargetInfo* outTarget) { Vector> memberCandidates; constexpr auto kDecorationBuiltIn = static_cast(spv::Decoration::BuiltIn); constexpr auto kBuiltInPosition = static_cast(spv::BuiltIn::Position); for (auto& inst : context->module()->annotations()) { if (inst.opcode() == spv::Op::OpDecorate) { if (inst.NumInOperands() < 3) continue; if (inst.GetSingleWordInOperand(1) != kDecorationBuiltIn) continue; if (inst.GetSingleWordInOperand(2) != kBuiltInPosition) continue; if (ResolveDirectPositionTarget(context, inst.GetSingleWordInOperand(0), outTarget)) return true; } else if (inst.opcode() == spv::Op::OpMemberDecorate) { if (inst.NumInOperands() < 4) continue; if (inst.GetSingleWordInOperand(2) != kDecorationBuiltIn) continue; if (inst.GetSingleWordInOperand(3) != kBuiltInPosition) continue; memberCandidates.emplace_back(inst.GetSingleWordInOperand(0), inst.GetSingleWordInOperand(1)); } } for (const auto& [structTypeId, memberIndex] : memberCandidates) { if (ResolveMemberPositionTarget(context, structTypeId, memberIndex, outTarget)) return true; } return false; } Bool InsertPositionFixup(spvtools::opt::IRContext* context, spvtools::opt::Instruction* insertBefore, const PositionTargetInfo& target, Uint32 halfConstId, Bool doYFlip, Bool doZRemap, Bool doSurfaceRotate90, Bool doSurfaceRotate180, Bool doSurfaceRotate270) { using namespace spvtools::opt; InstructionBuilder builder(context, insertBefore, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); Uint32 positionPtrId = target.variableId; if (target.isMember) { const Uint32 memberIndexId = builder.GetUintConstantId(target.memberIndex); if (memberIndexId == 0) return false; auto* access = builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId}); if (!access) return false; positionPtrId = access->result_id(); } auto* position = builder.AddLoad(target.vectorTypeId, positionPtrId); if (!position) return false; auto* x = builder.AddCompositeExtract(target.floatTypeId, position->result_id(), {0}); auto* y = builder.AddCompositeExtract(target.floatTypeId, position->result_id(), {1}); auto* z = builder.AddCompositeExtract(target.floatTypeId, position->result_id(), {2}); auto* w = builder.AddCompositeExtract(target.floatTypeId, position->result_id(), {3}); if (!x || !y || !z || !w) return false; if (!doYFlip && !doZRemap && !doSurfaceRotate90 && !doSurfaceRotate180 && !doSurfaceRotate270) { return false; } Uint32 xValueId = x->result_id(); Uint32 yValueId = y->result_id(); if (doYFlip) { auto* negY = builder.AddUnaryOp(target.floatTypeId, spv::Op::OpFNegate, y->result_id()); if (!negY) return false; yValueId = negY->result_id(); } if (doSurfaceRotate90) { auto* negY = builder.AddUnaryOp(target.floatTypeId, spv::Op::OpFNegate, yValueId); if (!negY) return false; xValueId = negY->result_id(); yValueId = x->result_id(); } else if (doSurfaceRotate180) { auto* negX = builder.AddUnaryOp(target.floatTypeId, spv::Op::OpFNegate, xValueId); auto* negY = builder.AddUnaryOp(target.floatTypeId, spv::Op::OpFNegate, yValueId); if (!negX || !negY) return false; xValueId = negX->result_id(); yValueId = negY->result_id(); } else if (doSurfaceRotate270) { auto* negX = builder.AddUnaryOp(target.floatTypeId, spv::Op::OpFNegate, xValueId); if (!negX) return false; xValueId = yValueId; yValueId = negX->result_id(); } Uint32 zValueId = z->result_id(); if (doZRemap) { auto* zPlusW = builder.AddBinaryOp(target.floatTypeId, spv::Op::OpFAdd, z->result_id(), w->result_id()); if (!zPlusW) return false; auto* mappedZ = builder.AddBinaryOp(target.floatTypeId, spv::Op::OpFMul, zPlusW->result_id(), halfConstId); if (!mappedZ) return false; zValueId = mappedZ->result_id(); } auto* fixedPosition = builder.AddCompositeConstruct(target.vectorTypeId, {xValueId, yValueId, zValueId, w->result_id()}); if (!fixedPosition) return false; return builder.AddStore(positionPtrId, fixedPosition->result_id()) != nullptr; } class GlToVulkanPositionFixPass final : public spvtools::opt::Pass { public: const char* name() const override { return "gl-to-vulkan-position-fix"; } explicit GlToVulkanPositionFixPass(ProgramFactory::CompileOptionFlags transformFlags) : m_transformFlags(transformFlags) {} Status Process() override { if (!m_transformFlags) return Status::SuccessWithoutChange; PositionTargetInfo target{}; if (!FindPositionTarget(context(), &target)) return Status::SuccessWithoutChange; auto* floatType = context()->get_type_mgr()->GetType(target.floatTypeId); if (!floatType) return Status::SuccessWithoutChange; const auto halfBits = std::bit_cast(0.5f); const auto* halfConst = context()->get_constant_mgr()->GetConstant(floatType, {halfBits}); auto* halfInst = context()->get_constant_mgr()->GetDefiningInstruction(halfConst); if (!halfInst) return Status::SuccessWithoutChange; const Uint32 halfConstId = halfInst->result_id(); const Bool doYFlip = (m_transformFlags & ProgramFactory::CompileOptionBit::PositionYFlip); const Bool doZRemap = (m_transformFlags & ProgramFactory::CompileOptionBit::PositionZRemap); const Bool doSurfaceRotate90 = (m_transformFlags & ProgramFactory::CompileOptionBit::SurfaceRotate90); const Bool doSurfaceRotate180 = (m_transformFlags & ProgramFactory::CompileOptionBit::SurfaceRotate180); const Bool doSurfaceRotate270 = (m_transformFlags & ProgramFactory::CompileOptionBit::SurfaceRotate270); Bool modified = false; for (auto& entryPoint : get_module()->entry_points()) { if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue; if (entryPoint.NumInOperands() < 2) continue; const auto model = static_cast(entryPoint.GetSingleWordInOperand(0)); if (model != spv::ExecutionModel::Vertex && model != spv::ExecutionModel::TessellationEvaluation && model != spv::ExecutionModel::Geometry) { continue; } auto* function = context()->GetFunction(entryPoint.GetSingleWordInOperand(1)); if (!function) continue; for (auto& bb : *function) { for (auto instIter = bb.begin(); instIter != bb.end(); ++instIter) { auto* inst = &*instIter; const Bool needsFixup = (model == spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpEmitVertex) || (model != spv::ExecutionModel::Geometry && inst->opcode() == spv::Op::OpReturn); if (!needsFixup) continue; modified |= InsertPositionFixup(context(), inst, target, halfConstId, doYFlip, doZRemap, doSurfaceRotate90, doSurfaceRotate180, doSurfaceRotate270); } } } if (!modified) return Status::SuccessWithoutChange; context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisDefUse | spvtools::opt::IRContext::kAnalysisInstrToBlockMapping); return Status::SuccessWithChange; } private: ProgramFactory::CompileOptionFlags m_transformFlags; }; // gl_FragCoord back into GL's window space, for default-framebuffer draws only. // // Vulkan's gl_FragCoord.y is the framebuffer ROW being written - not a value the // viewport rect can move independently of placement. The default framebuffer's image is // stored display-side-up and the vertex stage compensates by negating gl_Position.y, so // for every default-FBO draw the framebuffer row of a fragment is exactly // `height - y_GL` (the viewport terms cancel: yf_VK = H - yf_GL for any viewport rect). // A shader that reads gl_FragCoord therefore sees a flipped Y, and once the viewport // rect started being converted to the stored orientation it also sees a Y that is // OUTSIDE the range GL promises - a 32-pixel-tall viewport at GL y=0 reports 224..255 on // a 256-tall surface. GL CTS shader_image_load_store writes imageStore(image, // ivec2(gl_FragCoord.xy)) into an image exactly the size of that viewport, so every // store fell outside the image and the test read back zeroes. // // The rewrite redirects every read of the builtin to a Private copy initialised once at // entry, which is exact for all access forms (whole-vector loads, `.y` access chains, // OpCopyMemory) and leaves the builtin itself - and its decorations - untouched. class GlFragCoordYFlipPass final : public spvtools::opt::Pass { public: const char* name() const override { return "mobilegl-fragcoord-y-flip"; } explicit GlFragCoordYFlipPass(Uint32 framebufferHeight) : m_framebufferHeight(framebufferHeight) {} Status Process() override { using namespace spvtools::opt; if (m_framebufferHeight == 0) return Status::SuccessWithoutChange; Instruction* entryPoint = nullptr; for (auto& candidate : get_module()->entry_points()) { if (candidate.NumInOperands() >= 2 && static_cast(candidate.GetSingleWordInOperand(0)) == spv::ExecutionModel::Fragment) { entryPoint = &candidate; break; } } if (!entryPoint) return Status::SuccessWithoutChange; const Uint32 builtinVarId = FindFragCoordVariable(); if (builtinVarId == 0) return Status::SuccessWithoutChange; Instruction* builtinVar = context()->get_def_use_mgr()->GetDef(builtinVarId); if (!builtinVar || builtinVar->opcode() != spv::Op::OpVariable) return Status::SuccessWithoutChange; // The builtin is `Input vec4`; take the vector and component types from its own // pointer type rather than assuming float32x4, so a module that spells it // differently declines instead of miscompiling. Instruction* inputPtrType = context()->get_def_use_mgr()->GetDef(builtinVar->type_id()); if (!inputPtrType || inputPtrType->opcode() != spv::Op::OpTypePointer) { return Status::SuccessWithoutChange; } const Uint32 vectorTypeId = inputPtrType->GetSingleWordInOperand(1); Instruction* vectorType = context()->get_def_use_mgr()->GetDef(vectorTypeId); if (!vectorType || vectorType->opcode() != spv::Op::OpTypeVector || vectorType->GetSingleWordInOperand(1) != 4) { return Status::SuccessWithoutChange; } const Uint32 floatTypeId = vectorType->GetSingleWordInOperand(0); auto* floatType = context()->get_type_mgr()->GetType(floatTypeId); if (!floatType || !floatType->AsFloat() || floatType->AsFloat()->width() != 32) { return Status::SuccessWithoutChange; } const auto heightBits = std::bit_cast(static_cast(m_framebufferHeight)); const auto* heightConst = context()->get_constant_mgr()->GetConstant(floatType, {heightBits}); auto* heightInst = context()->get_constant_mgr()->GetDefiningInstruction(heightConst); if (!heightInst) return Status::SuccessWithoutChange; auto* function = context()->GetFunction(entryPoint->GetSingleWordInOperand(1)); if (!function || function->begin() == function->end()) return Status::SuccessWithoutChange; const Uint32 privatePtrTypeId = context()->get_type_mgr()->FindPointerToType(vectorTypeId, spv::StorageClass::Private); if (privatePtrTypeId == 0) return Status::SuccessWithoutChange; const Uint32 copyVarId = context()->TakeNextId(); if (copyVarId == 0) return Status::SuccessWithoutChange; auto copyVar = std::make_unique( context(), spv::Op::OpVariable, privatePtrTypeId, copyVarId, std::initializer_list{ {SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast(spv::StorageClass::Private)}}}); context()->AddGlobalValue(std::move(copyVar)); // Redirect the reads BEFORE emitting the initialiser, so the initialiser's own // load of the builtin is not rewritten into a load of the (still empty) copy. if (!RedirectReads(builtinVarId, copyVarId)) return Status::SuccessWithoutChange; auto& entryBlock = *function->begin(); auto insertPoint = entryBlock.begin(); while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) { ++insertPoint; } if (insertPoint == entryBlock.end()) return Status::SuccessWithoutChange; InstructionBuilder builder(context(), &*insertPoint, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); auto* raw = builder.AddLoad(vectorTypeId, builtinVarId); if (!raw) return Status::SuccessWithoutChange; auto* x = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {0}); auto* y = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {1}); auto* z = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {2}); auto* w = builder.AddCompositeExtract(floatTypeId, raw->result_id(), {3}); if (!x || !y || !z || !w) return Status::SuccessWithoutChange; auto* flippedY = builder.AddBinaryOp(floatTypeId, spv::Op::OpFSub, heightInst->result_id(), y->result_id()); if (!flippedY) return Status::SuccessWithoutChange; auto* corrected = builder.AddCompositeConstruct( vectorTypeId, {x->result_id(), flippedY->result_id(), z->result_id(), w->result_id()}); if (!corrected) return Status::SuccessWithoutChange; if (!builder.AddStore(copyVarId, corrected->result_id())) return Status::SuccessWithoutChange; // SPIR-V 1.4 widened the entry-point interface to every global the entry point // statically uses, Private included; earlier versions accept Input/Output only, // so listing it there would be invalid. if (get_module()->version() >= 0x00010400u) { entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {copyVarId}}); context()->AnalyzeUses(entryPoint); } context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisDefUse | spvtools::opt::IRContext::kAnalysisInstrToBlockMapping); return Status::SuccessWithChange; } private: Uint32 FindFragCoordVariable() const { for (const auto& annotation : get_module()->annotations()) { if (annotation.opcode() != spv::Op::OpDecorate) continue; if (annotation.NumInOperands() < 3) continue; if (static_cast(annotation.GetSingleWordInOperand(1)) != spv::Decoration::BuiltIn) { continue; } if (static_cast(annotation.GetSingleWordInOperand(2)) != spv::BuiltIn::FragCoord) { continue; } return annotation.GetSingleWordInOperand(0); } return 0; } // Every instruction that reads through the builtin's POINTER gets the copy instead. // Decorations, names and the entry-point interface keep naming the builtin. Bool RedirectReads(Uint32 builtinVarId, Uint32 copyVarId) { using namespace spvtools::opt; Bool ok = true; Vector users; context()->get_def_use_mgr()->ForEachUser(builtinVarId, [&](Instruction* user) { switch (user->opcode()) { case spv::Op::OpLoad: case spv::Op::OpAccessChain: case spv::Op::OpInBoundsAccessChain: case spv::Op::OpPtrAccessChain: case spv::Op::OpInBoundsPtrAccessChain: case spv::Op::OpCopyMemory: case spv::Op::OpCopyMemorySized: users.push_back(user); break; case spv::Op::OpStore: // gl_FragCoord is read-only; a store through it means this is not the // module we think it is. ok = false; break; default: break; } }); if (!ok) return false; for (Instruction* user : users) { for (Uint32 i = 0; i < user->NumInOperands(); ++i) { auto& operand = user->GetInOperand(i); if (operand.type == SPV_OPERAND_TYPE_ID && !operand.words.empty() && operand.words[0] == builtinVarId) { operand.words[0] = copyVarId; } } context()->AnalyzeUses(user); } return true; } Uint32 m_framebufferHeight = 0; }; // Decorates the module's captured varyings for VK_EXT_transform_feedback: // user outputs get XfbBuffer/XfbStride/Offset directly; a captured // gl_Position (a gl_PerVertex member) is mirrored into a dedicated output // variable copied before every OpReturn, BEFORE the position fixup runs, // so the captured value is the shader's own (pre-remap) gl_Position. class XfbCaptureDecoratePass final : public spvtools::opt::Pass { public: struct CapturedVarying { std::string name; Uint32 bufferIndex = 0; Uint32 offsetBytes = 0; // Set when the capture names a member of an output interface block // ("Block.member"): the decoration target is then the block's struct TYPE, // decorated per member, not the variable. `name` keeps the GL spelling and // is useless for the id lookup, so the instance name is carried separately. std::string blockInstanceName; std::string blockName; Int blockMemberIndex = -1; Int blockMemberElement = -1; // array element of that member, -1 = the whole member Uint32 byteSize = 0; }; const char* name() const override { return "mobilegl-xfb-capture-decorate"; } XfbCaptureDecoratePass(Vector varyings, Vector strides) : m_varyings(Move(varyings)), m_strides(Move(strides)) {} Status Process() override { using namespace spvtools::opt; if (m_varyings.empty()) return Status::SuccessWithoutChange; auto entryPointIter = get_module()->entry_points().begin(); if (entryPointIter == get_module()->entry_points().end()) return Status::SuccessWithoutChange; spvtools::opt::Instruction* entryPoint = &*entryPointIter; const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1); // Name -> result id map from the debug section. std::unordered_map idsByName; for (auto& debugInst : get_module()->debugs2()) { if (debugInst.opcode() != spv::Op::OpName) continue; idsByName[debugInst.GetInOperand(1).AsString()] = debugInst.GetSingleWordInOperand(0); } auto* decorationManager = context()->get_decoration_mgr(); const auto decorateForXfb = [&](Uint32 targetId, Uint32 bufferIndex, Uint32 offsetBytes) { const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0; decorationManager->AddDecorationVal(targetId, static_cast(spv::Decoration::XfbBuffer), bufferIndex); decorationManager->AddDecorationVal(targetId, static_cast(spv::Decoration::XfbStride), stride); decorationManager->AddDecorationVal(targetId, static_cast(spv::Decoration::Offset), offsetBytes); }; // SPIR-V puts XfbBuffer/XfbStride/Offset on the struct MEMBER when the // captured varying lives in an interface block (SPIR-V 1.6 ยง3.20 lists all // three as member-decoratable); Offset in particular is illegal on the block // variable once the type is decorated Block. const auto decorateMemberForXfb = [&](Uint32 structTypeId, Uint32 memberIndex, Uint32 bufferIndex, Uint32 offsetBytes) { const Uint32 stride = bufferIndex < m_strides.size() ? m_strides[bufferIndex] : 0; decorationManager->AddMemberDecoration(structTypeId, memberIndex, static_cast(spv::Decoration::XfbBuffer), bufferIndex); decorationManager->AddMemberDecoration(structTypeId, memberIndex, static_cast(spv::Decoration::XfbStride), stride); decorationManager->AddMemberDecoration(structTypeId, memberIndex, static_cast(spv::Decoration::Offset), offsetBytes); }; // A member array captured element by element ("Block.attrib[0]" .. "[15]") // is one SPIR-V member, so its captures collapse into a single decoration // placed at the first element's offset - the rest follow from the member's // own layout. Collected first so the group is complete before it decorates. struct MemberGroup { Uint32 bufferIndex = 0; Uint32 minOffset = 0; Uint32 elementBytes = 0; Vector offsets; }; std::map, MemberGroup> memberGroups; Bool modified = false; Bool needsPositionMirror = false; Uint32 positionBufferIndex = 0; Uint32 positionOffset = 0; for (const auto& varying : m_varyings) { if (varying.name == "gl_Position") { needsPositionMirror = true; positionBufferIndex = varying.bufferIndex; positionOffset = varying.offsetBytes; continue; } if (varying.blockMemberIndex >= 0) { // glslang names the block's instance variable and its struct type // separately; an anonymous instance leaves only the type named, so // both spellings are tried before giving up. Uint32 structTypeId = 0; if (const auto it = idsByName.find(varying.blockInstanceName); it != idsByName.end()) { structTypeId = BlockStructTypeOf(it->second); } if (structTypeId == 0) { if (const auto it = idsByName.find(varying.blockName); it != idsByName.end()) { const spvtools::opt::Instruction* def = context()->get_def_use_mgr()->GetDef(it->second); if (def != nullptr && def->opcode() == spv::Op::OpTypeStruct) { structTypeId = it->second; } else if (def != nullptr && def->opcode() == spv::Op::OpVariable) { structTypeId = BlockStructTypeOf(it->second); } } } if (structTypeId == 0) { MGLOG_E("XfbCaptureDecoratePass: no SPIR-V interface block '%s' (instance '%s') for " "capture '%s'", varying.blockName.c_str(), varying.blockInstanceName.c_str(), varying.name.c_str()); continue; } auto& group = memberGroups[{structTypeId, static_cast(varying.blockMemberIndex)}]; if (group.offsets.empty() || varying.offsetBytes < group.minOffset) { group.minOffset = varying.offsetBytes; } group.bufferIndex = varying.bufferIndex; group.elementBytes = varying.byteSize; group.offsets.push_back(varying.offsetBytes); continue; } const auto idIt = idsByName.find(varying.name); if (idIt == idsByName.end()) { MGLOG_E("XfbCaptureDecoratePass: no SPIR-V variable named '%s'", varying.name.c_str()); continue; } decorateForXfb(idIt->second, varying.bufferIndex, varying.offsetBytes); modified = true; } for (auto& [key, group] : memberGroups) { // The single Offset can only stand for the whole group when the group's // captures are a gap-free ascending run - that is what SPIR-V lays the // member's elements out as. Anything else still gets a best-effort // decoration, but say so, because the capture layout will not match GL. std::sort(group.offsets.begin(), group.offsets.end()); for (SizeT i = 1; i < group.offsets.size(); ++i) { if (group.elementBytes == 0 || group.offsets[i] != group.offsets[i - 1] + group.elementBytes) { MGLOG_D("XfbCaptureDecoratePass: block member %u of type %%%u is captured with a " "non-contiguous element set; the capture layout will differ from GL's", key.second, key.first); break; } } decorateMemberForXfb(key.first, key.second, group.bufferIndex, group.minOffset); modified = true; } if (needsPositionMirror) { modified |= MirrorPositionForCapture(entryFunctionId, *entryPoint, positionBufferIndex, positionOffset, decorateForXfb); } if (!modified) return Status::SuccessWithoutChange; context()->AddCapability(spv::Capability::TransformFeedback); { auto executionMode = MakeUnique( context(), spv::Op::OpExecutionMode, 0, 0, std::initializer_list{ {SPV_OPERAND_TYPE_ID, {entryPoint->GetSingleWordInOperand(1)}}, {SPV_OPERAND_TYPE_EXECUTION_MODE, {static_cast(spv::ExecutionMode::Xfb)}}}); get_module()->AddExecutionMode(Move(executionMode)); } context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone); return Status::SuccessWithChange; } private: // The struct type an interface-block variable points at, peeling an array of // block instances on the way. 0 when the id is not a block variable at all. Uint32 BlockStructTypeOf(Uint32 variableId) { auto* defUse = context()->get_def_use_mgr(); const spvtools::opt::Instruction* variable = defUse->GetDef(variableId); if (variable == nullptr || variable->opcode() != spv::Op::OpVariable) return 0; const spvtools::opt::Instruction* pointer = defUse->GetDef(variable->type_id()); if (pointer == nullptr || pointer->opcode() != spv::Op::OpTypePointer) return 0; Uint32 pointeeId = pointer->GetSingleWordInOperand(1); for (const spvtools::opt::Instruction* pointee = defUse->GetDef(pointeeId); pointee != nullptr; pointee = defUse->GetDef(pointeeId)) { if (pointee->opcode() == spv::Op::OpTypeStruct) return pointeeId; if (pointee->opcode() != spv::Op::OpTypeArray && pointee->opcode() != spv::Op::OpTypeRuntimeArray) { return 0; } pointeeId = pointee->GetSingleWordInOperand(0); } return 0; } template Bool MirrorPositionForCapture(Uint32 entryFunctionId, spvtools::opt::Instruction& entryPoint, Uint32 bufferIndex, Uint32 offsetBytes, const DecorateFn& decorateForXfb) { const Uint32 entryPointModel = entryPoint.GetSingleWordInOperand(0); using namespace spvtools::opt; PositionTargetInfo target{}; if (!FindPositionTarget(context(), &target)) { MGLOG_E("XfbCaptureDecoratePass: gl_Position capture requested but no position output found"); return false; } if (!target.isMember) { // Standalone gl_Position variable: decorate it directly. decorateForXfb(target.variableId, bufferIndex, offsetBytes); return true; } auto* typeManager = context()->get_type_mgr(); const Uint32 mirrorPointerTypeId = typeManager->FindPointerToType(target.vectorTypeId, spv::StorageClass::Output); if (mirrorPointerTypeId == 0) return false; const Uint32 mirrorVariableId = context()->TakeNextId(); auto mirrorVariable = MakeUnique( context(), spv::Op::OpVariable, mirrorPointerTypeId, mirrorVariableId, std::initializer_list{ {SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast(spv::StorageClass::Output)}}}); get_module()->AddGlobalValue(Move(mirrorVariable)); // A free output location: past every explicitly decorated output. Uint32 mirrorLocation = 0; for (auto& annotation : get_module()->annotations()) { if (annotation.opcode() != spv::Op::OpDecorate || annotation.GetSingleWordInOperand(1) != static_cast(spv::Decoration::Location)) { continue; } mirrorLocation = std::max(mirrorLocation, annotation.GetSingleWordInOperand(2) + 1); } auto* decorationManager = context()->get_decoration_mgr(); decorationManager->AddDecorationVal(mirrorVariableId, static_cast(spv::Decoration::Location), mirrorLocation); decorateForXfb(mirrorVariableId, bufferIndex, offsetBytes); entryPoint.AddOperand({SPV_OPERAND_TYPE_ID, {mirrorVariableId}}); auto* function = context()->GetFunction(entryFunctionId); if (function == nullptr) return false; const auto model = static_cast(entryPointModel); Bool injected = false; for (auto& block : *function) { for (auto instIter = block.begin(); instIter != block.end(); ++instIter) { // Geometry stages capture per emitted vertex; other stages at return. const Bool isInjectionSite = model == spv::ExecutionModel::Geometry ? instIter->opcode() == spv::Op::OpEmitVertex : instIter->opcode() == spv::Op::OpReturn; if (!isInjectionSite) continue; InstructionBuilder builder(context(), &*instIter, IRContext::kAnalysisNone); const Uint32 memberIndexId = builder.GetUintConstantId(target.memberIndex); auto* access = builder.AddAccessChain(target.vectorPtrTypeId, target.variableId, {memberIndexId}); if (access == nullptr) return injected; auto* value = builder.AddLoad(target.vectorTypeId, access->result_id()); if (value == nullptr) return injected; builder.AddStore(mirrorVariableId, value->result_id()); injected = true; } } return injected; } Vector m_varyings; Vector m_strides; }; // Adreno 650 (driver 512.502) faults the GPU on an implicit-LOD sample of a full-screen // colour render target: the texture unit's derivative path reads outside the image's // allocation even though the sampler clamps LOD to 0 and the mapping is 1:1. MobileGL's // own default-framebuffer blit shader works around it with textureLod, but an // application's shader (Minecraft's blit.fsh is `texture(InSampler, texCoord)`) cannot be // edited - so rewrite the sample at the SPIR-V level instead. // // The rewrite is only requested for draws whose every sampler binding is clamped to one // mip level, where explicit LOD 0 is exactly what the implicit form must already produce: // lambda' = clamp(lambda + bias, minLod, maxLod) with minLod = maxLod = 0. Bias and MinLod // operands are therefore dropped rather than translated. class ForceExplicitLod0SamplePass final : public spvtools::opt::Pass { public: const char* name() const override { return "force-explicit-lod0-sample"; } Status Process() override { Bool isFragment = false; for (auto& entryPoint : get_module()->entry_points()) { if (entryPoint.opcode() != spv::Op::OpEntryPoint) continue; if (static_cast(entryPoint.GetSingleWordInOperand(0)) == spv::ExecutionModel::Fragment) { isFragment = true; break; } } if (!isFragment) return Status::SuccessWithoutChange; // Plan first, mutate second. Materializing the LOD constant is itself a module // change, so it must not happen unless at least one rewrite is going to follow - // otherwise the pass would grow the binary while reporting SuccessWithoutChange. Vector plans; for (auto& function : *get_module()) { for (auto& block : function) { for (auto& inst : block) { RewritePlan plan{}; if (PlanRewrite(&inst, plan)) plans.push_back(Move(plan)); } } } if (plans.empty()) return Status::SuccessWithoutChange; const Uint32 zeroId = GetFloatZeroId(); if (zeroId == 0) return Status::SuccessWithoutChange; for (auto& plan : plans) { plan.operands.push_back({SPV_OPERAND_TYPE_ID, {zeroId}}); for (auto& operand : plan.trailingOperands) { plan.operands.push_back(operand); } plan.instruction->SetOpcode(plan.opcode); plan.instruction->SetInOperands(Move(plan.operands)); } // Opcodes and operand lists changed underneath every cached analysis. context()->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone); return Status::SuccessWithChange; } private: struct RewritePlan { spvtools::opt::Instruction* instruction = nullptr; spv::Op opcode = spv::Op::OpNop; // Everything up to and including the Image Operands mask; the Lod id and the // trailing operand values are appended once the constant exists. Vector operands; Vector trailingOperands; }; // Image Operands bits that may accompany an implicit-LOD sample, in the canonical // ascending order SPIR-V requires the operand values to appear in. static constexpr Uint32 kBias = 0x1; static constexpr Uint32 kLod = 0x2; static constexpr Uint32 kGrad = 0x4; static constexpr Uint32 kConstOffset = 0x8; static constexpr Uint32 kOffset = 0x10; static constexpr Uint32 kConstOffsets = 0x20; static constexpr Uint32 kSample = 0x40; static constexpr Uint32 kMinLod = 0x80; static constexpr Uint32 kKnownMask = 0xFF; Uint32 GetFloatZeroId() { // Reuse a 32-bit float type already in the module; a shader that samples always has // one, and looking it up avoids depending on type-creation API details. Uint32 floatTypeId = 0; for (auto& inst : get_module()->types_values()) { if (inst.opcode() == spv::Op::OpTypeFloat && inst.NumInOperands() >= 1 && inst.GetSingleWordInOperand(0) == 32) { floatTypeId = inst.result_id(); break; } } if (floatTypeId == 0) return 0; const auto* floatType = context()->get_type_mgr()->GetType(floatTypeId); if (floatType == nullptr) return 0; const auto zeroBits = std::bit_cast(0.0f); const auto* zeroConst = context()->get_constant_mgr()->GetConstant(floatType, {zeroBits}); if (zeroConst == nullptr) return 0; auto* zeroInst = context()->get_constant_mgr()->GetDefiningInstruction(zeroConst); return zeroInst != nullptr ? zeroInst->result_id() : 0; } static Bool MapOpcode(spv::Op op, spv::Op& outOpcode, Uint32& outFixedOperandCount) { switch (op) { case spv::Op::OpImageSampleImplicitLod: outOpcode = spv::Op::OpImageSampleExplicitLod; outFixedOperandCount = 2; // sampled image, coordinate return true; case spv::Op::OpImageSampleProjImplicitLod: outOpcode = spv::Op::OpImageSampleProjExplicitLod; outFixedOperandCount = 2; return true; case spv::Op::OpImageSampleDrefImplicitLod: outOpcode = spv::Op::OpImageSampleDrefExplicitLod; outFixedOperandCount = 3; // sampled image, coordinate, Dref return true; case spv::Op::OpImageSampleProjDrefImplicitLod: outOpcode = spv::Op::OpImageSampleProjDrefExplicitLod; outFixedOperandCount = 3; return true; default: return false; } } static Bool PlanRewrite(spvtools::opt::Instruction* inst, RewritePlan& outPlan) { spv::Op newOpcode = spv::Op::OpNop; Uint32 fixedCount = 0; if (!MapOpcode(inst->opcode(), newOpcode, fixedCount)) return false; if (inst->NumInOperands() < fixedCount) return false; Uint32 mask = 0; Uint32 next = fixedCount; if (inst->NumInOperands() > fixedCount) { mask = inst->GetSingleWordInOperand(fixedCount); next = fixedCount + 1; } // An operand this pass does not model would be silently reordered or dropped, and // Grad cannot legally accompany an implicit-LOD sample: leave such an instruction be. if ((mask & ~kKnownMask) != 0 || (mask & kGrad) != 0) return false; Vector fixedOperands; fixedOperands.reserve(fixedCount + 1); for (Uint32 i = 0; i < fixedCount; ++i) { fixedOperands.push_back(inst->GetInOperand(i)); } // Collect the surviving operand values in the same ascending-bit order they were // encoded in, so the rebuilt list stays canonical. Uint32 keptMask = kLod; Vector keptOperands; static constexpr Uint32 kOrderedBits[] = {kBias, kLod, kGrad, kConstOffset, kOffset, kConstOffsets, kSample, kMinLod}; for (const Uint32 bit : kOrderedBits) { if ((mask & bit) == 0) continue; if (next >= inst->NumInOperands()) return false; const spvtools::opt::Operand value = inst->GetInOperand(next++); // Bias and MinLod only shift a lambda that is already clamped to 0, and any // original Lod is replaced by the constant the caller appends. if (bit == kBias || bit == kMinLod || bit == kLod) continue; keptMask |= bit; keptOperands.push_back(value); } fixedOperands.push_back({SPV_OPERAND_TYPE_IMAGE, {keptMask}}); outPlan.instruction = inst; outPlan.opcode = newOpcode; outPlan.operands = Move(fixedOperands); outPlan.trailingOperands = Move(keptOperands); return true; } }; spvtools::Optimizer::PassToken CreateForceExplicitLod0SamplePass() { return spvtools::Optimizer::PassToken(MakeUnique()); } Bool TransformSpirvForExplicitLod0Sampling(const Vector& input, Vector& output) { if (input.empty()) { output.clear(); return true; } spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3); spvtools::OptimizerOptions options; // Always off: the optimizer's input validator conflates "input invalid" with // "transform failed", and this call site fails open. Validating lanes check the // FINAL module via ValidateTransformedSpirv, which latches instead of rerouting // control flow. options.set_run_validator(false); optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* message) { MGLOG_E("Vulkan: explicit-LOD0 pass: %s", message != nullptr ? message : ""); }); optimizer.RegisterPass(CreateForceExplicitLod0SamplePass()); const Bool success = optimizer.Run(input.data(), input.size(), &output, options); if (!success) { MGLOG_E("Vulkan: explicit-LOD0 sampling pass failed; keeping the original module"); output = input; } return success; } spvtools::Optimizer::PassToken CreateGlToVulkanPositionFixPass( ProgramFactory::CompileOptionFlags transformFlags) { return spvtools::Optimizer::PassToken(MakeUnique(transformFlags)); } Bool TransformSpirvForFragCoordYFlip(const Vector& input, Vector& output, Uint32 framebufferHeight) { if (input.empty()) { output.clear(); return true; } if (framebufferHeight == 0) { output = input; return true; } spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3); spvtools::OptimizerOptions options; options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* message) { MGLOG_E("Vulkan: fragcoord y-flip pass: %s", message != nullptr ? message : ""); }); optimizer.RegisterPass( spvtools::Optimizer::PassToken(MakeUnique(framebufferHeight))); const Bool success = optimizer.Run(input.data(), input.size(), &output, options); if (!success) { MGLOG_E("Vulkan: failed to run the gl_FragCoord y-flip pass; keeping the original module"); output = input; } return success; } Bool TransformSpirvForXfbCapture(const Vector& input, Vector& output, const MG_State::GLState::ProgramObject& program) { if (input.empty()) { output.clear(); return true; } Vector varyings; varyings.reserve(program.GetTransformFeedbackVaryingCount()); for (const auto& varying : program.GetTransformFeedbackVaryings()) { varyings.push_back({varying.name, varying.bufferIndex, varying.offsetBytes, varying.blockInstanceName, varying.blockName, varying.blockMemberIndex, varying.blockMemberElement, varying.byteSize}); } Vector strides; strides.reserve(program.GetTransformFeedbackBufferCount()); for (SizeT i = 0; i < program.GetTransformFeedbackBufferCount(); ++i) { strides.push_back(program.GetTransformFeedbackStride(static_cast(i))); } spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3); spvtools::OptimizerOptions options; options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* message) { MGLOG_E("Vulkan: xfb capture pass: %s", message != nullptr ? message : ""); }); optimizer.RegisterPass(spvtools::Optimizer::PassToken( MakeUnique(Move(varyings), Move(strides)))); const Bool success = optimizer.Run(input.data(), input.size(), &output, options); if (!success) { MGLOG_E("Vulkan: xfb capture decoration pass failed; keeping the original module"); output = input; } return success; } Bool TransformSpirvForVulkanPositionFix(const Vector& input, Vector& output, ProgramFactory::CompileOptionFlags transformFlags) { if (input.empty()) { output.clear(); return true; } if (!transformFlags) { output = input; return true; } spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3); spvtools::OptimizerOptions options; options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* message) { MGLOG_E("Vulkan: position fix pass: %s", message != nullptr ? message : ""); }); optimizer.RegisterPass(CreateGlToVulkanPositionFixPass(transformFlags)); const Bool success = optimizer.Run(input.data(), input.size(), &output, options); if (!success) { MGLOG_E("Vulkan: failed to run GL->Vulkan position fix pass"); output = input; } return success; } ShaderStage PickClipFixupStage(const Vector>& shaders) { Bool hasGeometry = false; Bool hasTessEval = false; Bool hasVertex = false; for (const auto& shader : shaders) { if (!shader) continue; const auto stage = shader->GetShaderStage(); hasGeometry |= (stage == ShaderStage::Geometry); hasTessEval |= (stage == ShaderStage::TessEval); hasVertex |= (stage == ShaderStage::Vertex); } if (hasGeometry) return ShaderStage::Geometry; if (hasTessEval) return ShaderStage::TessEval; if (hasVertex) return ShaderStage::Vertex; return ShaderStage::Unknown; } ProgramFactory::DescriptorBindingKind ReflectDescriptorTypeToBindingKind(SpvReflectDescriptorType descriptorType) { switch (descriptorType) { case SPV_REFLECT_DESCRIPTOR_TYPE_UNIFORM_BUFFER: return ProgramFactory::DescriptorBindingKind::UniformBufferDynamic; case SPV_REFLECT_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER: case SPV_REFLECT_DESCRIPTOR_TYPE_SAMPLED_IMAGE: return ProgramFactory::DescriptorBindingKind::CombinedImageSampler; case SPV_REFLECT_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER: return ProgramFactory::DescriptorBindingKind::UniformTexelBuffer; case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER: return ProgramFactory::DescriptorBindingKind::StorageTexelBuffer; case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_BUFFER: return ProgramFactory::DescriptorBindingKind::StorageBuffer; case SPV_REFLECT_DESCRIPTOR_TYPE_STORAGE_IMAGE: return ProgramFactory::DescriptorBindingKind::StorageImage; default: MOBILEGL_ASSERT(false, "ProgramFactory: unsupported reflected descriptor type %d", static_cast(descriptorType)); return ProgramFactory::DescriptorBindingKind::None; } } String NormalizeDescriptorName(const SpvReflectDescriptorBinding& binding, ProgramFactory::DescriptorBindingKind kind) { const char* rawName = binding.name; if ((kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic || kind == ProgramFactory::DescriptorBindingKind::StorageBuffer) && binding.type_description != nullptr && binding.type_description->type_name != nullptr) { rawName = binding.type_description->type_name; } String name = (rawName != nullptr) ? rawName : ""; if (name.empty()) { name = std::format("__mg_unnamed_descriptor_set{}_binding{}_id{}", binding.set, binding.binding, binding.spirv_id); MGLOG_W("ProgramFactory: descriptor has empty name; using generated name '%s' (type=%d)", name.c_str(), static_cast(binding.descriptor_type)); } if (kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler || kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer || kind == ProgramFactory::DescriptorBindingKind::StorageTexelBuffer || kind == ProgramFactory::DescriptorBindingKind::StorageImage) { const auto arraySuffix = name.find("[0]"); if (arraySuffix != String::npos) { name = name.substr(0, arraySuffix); } } return name; } Bool RemapDescriptorBindingsForVulkan(const Vector>& inputModules, Uint32 maxBindings, Vector>& outputModules) { outputModules = inputModules; Vector reflectModules(outputModules.size()); Vector reflectModuleValid(outputModules.size(), false); UnorderedMap assignedBindings; Uint32 nextBinding = 0; const auto destroyReflectModules = [&]() { for (SizeT moduleIndex = 0; moduleIndex < reflectModules.size(); ++moduleIndex) { if (!reflectModuleValid[moduleIndex]) { continue; } spvReflectDestroyShaderModule(&reflectModules[moduleIndex]); reflectModuleValid[moduleIndex] = false; } }; for (SizeT moduleIndex = 0; moduleIndex < outputModules.size(); ++moduleIndex) { auto& moduleSpv = outputModules[moduleIndex]; if (moduleSpv.empty()) { continue; } const SpvReflectResult createResult = spvReflectCreateShaderModule(moduleSpv.size() * sizeof(Uint), moduleSpv.data(), &reflectModules[moduleIndex]); MOBILEGL_ASSERT(createResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory: failed to create reflection module for stage %zu (result=%d)", moduleIndex, static_cast(createResult)); if (createResult != SPV_REFLECT_RESULT_SUCCESS) { destroyReflectModules(); return false; } reflectModuleValid[moduleIndex] = true; uint32_t bindingCount = 0; SpvReflectResult reflectResult = spvReflectEnumerateDescriptorBindings(&reflectModules[moduleIndex], &bindingCount, nullptr); MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory: failed to enumerate descriptor bindings for stage %zu (result=%d)", moduleIndex, static_cast(reflectResult)); if (reflectResult != SPV_REFLECT_RESULT_SUCCESS) { destroyReflectModules(); return false; } Vector bindings(bindingCount); if (bindingCount > 0) { reflectResult = spvReflectEnumerateDescriptorBindings(&reflectModules[moduleIndex], &bindingCount, bindings.data()); MOBILEGL_ASSERT( reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory: failed to fetch descriptor bindings for stage %zu (result=%d)", moduleIndex, static_cast(reflectResult)); if (reflectResult != SPV_REFLECT_RESULT_SUCCESS) { destroyReflectModules(); return false; } } std::sort(bindings.begin(), bindings.end(), [](const auto* lhs, const auto* rhs) { if (lhs->set != rhs->set) { return lhs->set < rhs->set; } if (lhs->binding != rhs->binding) { return lhs->binding < rhs->binding; } return lhs->spirv_id < rhs->spirv_id; }); for (auto* binding : bindings) { MOBILEGL_ASSERT(binding != nullptr, "ProgramFactory: null descriptor binding reflection record"); const auto kind = ReflectDescriptorTypeToBindingKind(binding->descriptor_type); // A descriptor ARRAY occupies one binding with descriptorCount = N, and is // supported for exactly the kinds that have a per-element resolve path in // UniformManager::BindProgramUniformBuffers: UBO instance arrays // (uniform Block {...} b[N];), storage-block instance arrays, image uniform // arrays, and combined-image-sampler arrays (uniform sampler2D s[N];). // Anything else - the two TEXEL buffer kinds are what remain, samplerBuffer[N] // and imageBuffer[N] - must fail program creation cleanly rather than continue // with corrupt state. Their per-draw path writes pTexelBufferView as the // address of a vector element sized for one descriptor per binding, so an // array would not merely be unresolved, it would dangle. // // Getting listed here is not cosmetic: a kind that is rejected leaves // GetOrCreateProgram's MOBILEGL_ASSERT(remapOk) as the only complaint, and // that assert compiles out above DEBUG - so a release build SILENTLY kept // glslang's per-stage auto-mapped binding numbers, skipping the cross-stage // unification and the set->0 normalisation this function exists to do. A // program with an image array plus any second descriptor got aliased // bindings out of that, and a DEBUG build trapped on the same program. // The refusal below is MGLOG_E and per-program-compile, so it reports every // program it declines. It spent time at MGLOG_I because the old level // ordering compiled E out of the builds that ship. const Bool arraySupportedForKind = kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic || kind == ProgramFactory::DescriptorBindingKind::StorageBuffer || kind == ProgramFactory::DescriptorBindingKind::StorageImage || kind == ProgramFactory::DescriptorBindingKind::CombinedImageSampler; if (binding->count != 1 && !arraySupportedForKind) { MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor " "kind (name='%s' count=%u type=%d)", binding->name ? binding->name : "", binding->count, static_cast(binding->descriptor_type)); destroyReflectModules(); return false; } DescriptorKey key{}; key.kind = kind; key.name = NormalizeDescriptorName(*binding, kind); Uint32 assignedBinding = 0; const auto it = assignedBindings.find(key); if (it == assignedBindings.end()) { MOBILEGL_ASSERT(nextBinding < maxBindings, "ProgramFactory: reflected descriptor count exceeded maxBindings (%u >= %u)", nextBinding, maxBindings); assignedBinding = nextBinding; assignedBindings.emplace(key, assignedBinding); ++nextBinding; } else { assignedBinding = it->second; } if (binding->binding != assignedBinding || binding->set != 0) { reflectResult = spvReflectChangeDescriptorBindingNumbers(&reflectModules[moduleIndex], binding, assignedBinding, 0); MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory: failed to remap descriptor '%s' in stage %zu (result=%d)", key.name.c_str(), moduleIndex, static_cast(reflectResult)); if (reflectResult != SPV_REFLECT_RESULT_SUCCESS) { destroyReflectModules(); return false; } } } } for (SizeT moduleIndex = 0; moduleIndex < outputModules.size(); ++moduleIndex) { if (!reflectModuleValid[moduleIndex]) { continue; } const Uint32 codeSizeBytes = spvReflectGetCodeSize(&reflectModules[moduleIndex]); MOBILEGL_ASSERT((codeSizeBytes % sizeof(Uint)) == 0, "ProgramFactory: reflected SPIR-V size is not word aligned for stage %zu", moduleIndex); const Uint32* code = spvReflectGetCode(&reflectModules[moduleIndex]); MOBILEGL_ASSERT(code != nullptr, "ProgramFactory: reflected SPIR-V code pointer is null for stage %zu", moduleIndex); outputModules[moduleIndex].assign(code, code + (codeSizeBytes / sizeof(Uint))); } destroyReflectModules(); return true; } TextureTarget ReflectImageTraitsToTextureTarget(const SpvReflectImageTraits& imageTraits) { switch (imageTraits.dim) { case SpvDim1D: return imageTraits.arrayed != 0 ? TextureTarget::Texture1DArray : TextureTarget::Texture1D; case SpvDim2D: if (imageTraits.ms != 0) { return imageTraits.arrayed != 0 ? TextureTarget::Texture2DMultisampleArray : TextureTarget::Texture2DMultisample; } return imageTraits.arrayed != 0 ? TextureTarget::Texture2DArray : TextureTarget::Texture2D; case SpvDim3D: return TextureTarget::Texture3D; case SpvDimCube: return imageTraits.arrayed != 0 ? TextureTarget::TextureCubeMapArray : TextureTarget::TextureCubeMap; case SpvDimBuffer: return TextureTarget::TextureBuffer; default: MOBILEGL_ASSERT(false, "ProgramFactory: unsupported sampler image dim %d", imageTraits.dim); return TextureTarget::Unknown; } } Bool IsFloatStorageImageUniformType(GLenum uniformType) { switch (uniformType) { case GL_IMAGE_1D: case GL_IMAGE_2D: case GL_IMAGE_3D: case GL_IMAGE_2D_RECT: case GL_IMAGE_CUBE: case GL_IMAGE_BUFFER: case GL_IMAGE_1D_ARRAY: case GL_IMAGE_2D_ARRAY: case GL_IMAGE_CUBE_MAP_ARRAY: case GL_IMAGE_2D_MULTISAMPLE: case GL_IMAGE_2D_MULTISAMPLE_ARRAY: return true; default: return false; } } } // 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) { return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInInstanceIndex); } // GL's gl_BaseVertex and Vulkan's BaseVertex agree for indexed draws and disagree for every // other command, so a program declaring the builtin needs the ZeroBaseVertex variant when a // non-indexed draw uses it (see CompileOptionBit::ZeroBaseVertex). "Declares" rather than // "reads" is the honest word and the useful one: the zeroing pass keeps the variable, so // both variants of a program answer this question identically. Bool ProgramFactory::ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule) { return ReflectedDeclaresInputBuiltin(reflectModule, SpvBuiltInBaseVertex); } // gl_ViewportIndex on the last pre-rasterization stage. glslang emits it natively for Vulkan // (BuiltIn ViewportIndex plus OpCapability MultiViewport), and nothing in the SpirvPasses // chain touches it, so a plain reflection of the declared output builtins is the whole test. Bool ProgramFactory::ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule) { return ReflectedDeclaresOutputBuiltin(reflectModule, SpvBuiltInViewportIndex); } Bool ProgramFactory::ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin) { for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) { const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex]; for (Uint32 variableIndex = 0; variableIndex < entryPoint.output_variable_count; ++variableIndex) { const SpvReflectInterfaceVariable* variable = entryPoint.output_variables[variableIndex]; if (variable != nullptr && (variable->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0 && variable->built_in == builtin) { return true; } } } return false; } Bool ProgramFactory::ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin) { 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 == builtin) { return true; } } } return false; } VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) { switch (stage) { case ShaderStage::Vertex: return VK_SHADER_STAGE_VERTEX_BIT; case ShaderStage::Fragment: return VK_SHADER_STAGE_FRAGMENT_BIT; case ShaderStage::Geometry: return VK_SHADER_STAGE_GEOMETRY_BIT; case ShaderStage::TessControl: return VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT; case ShaderStage::TessEval: return VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT; case ShaderStage::Compute: return VK_SHADER_STAGE_COMPUTE_BIT; default: return VK_SHADER_STAGE_ALL_GRAPHICS; } } VkFormat ProgramFactory::ConvertSpirvImageFormatToVkFormat(SpvImageFormat format) { switch (format) { case SpvImageFormatUnknown: return VK_FORMAT_UNDEFINED; case SpvImageFormatRgba32f: return VK_FORMAT_R32G32B32A32_SFLOAT; case SpvImageFormatRgba16f: return VK_FORMAT_R16G16B16A16_SFLOAT; case SpvImageFormatR32f: return VK_FORMAT_R32_SFLOAT; case SpvImageFormatRgba8: return VK_FORMAT_R8G8B8A8_UNORM; case SpvImageFormatRgba8Snorm: return VK_FORMAT_R8G8B8A8_SNORM; case SpvImageFormatRg32f: return VK_FORMAT_R32G32_SFLOAT; case SpvImageFormatRg16f: return VK_FORMAT_R16G16_SFLOAT; case SpvImageFormatR11fG11fB10f: return VK_FORMAT_B10G11R11_UFLOAT_PACK32; case SpvImageFormatR16f: return VK_FORMAT_R16_SFLOAT; case SpvImageFormatRgba16: return VK_FORMAT_R16G16B16A16_UNORM; case SpvImageFormatRgb10A2: return VK_FORMAT_A2R10G10B10_UNORM_PACK32; case SpvImageFormatRg16: return VK_FORMAT_R16G16_UNORM; case SpvImageFormatRg8: return VK_FORMAT_R8G8_UNORM; case SpvImageFormatR16: return VK_FORMAT_R16_UNORM; case SpvImageFormatR8: return VK_FORMAT_R8_UNORM; case SpvImageFormatRgba16Snorm: return VK_FORMAT_R16G16B16A16_SNORM; case SpvImageFormatRg16Snorm: return VK_FORMAT_R16G16_SNORM; case SpvImageFormatRg8Snorm: return VK_FORMAT_R8G8_SNORM; case SpvImageFormatR16Snorm: return VK_FORMAT_R16_SNORM; case SpvImageFormatR8Snorm: return VK_FORMAT_R8_SNORM; case SpvImageFormatRgba32i: return VK_FORMAT_R32G32B32A32_SINT; case SpvImageFormatRgba16i: return VK_FORMAT_R16G16B16A16_SINT; case SpvImageFormatRgba8i: return VK_FORMAT_R8G8B8A8_SINT; case SpvImageFormatR32i: return VK_FORMAT_R32_SINT; case SpvImageFormatRg32i: return VK_FORMAT_R32G32_SINT; case SpvImageFormatRg16i: return VK_FORMAT_R16G16_SINT; case SpvImageFormatRg8i: return VK_FORMAT_R8G8_SINT; case SpvImageFormatR16i: return VK_FORMAT_R16_SINT; case SpvImageFormatR8i: return VK_FORMAT_R8_SINT; case SpvImageFormatRgba32ui: return VK_FORMAT_R32G32B32A32_UINT; case SpvImageFormatRgba16ui: return VK_FORMAT_R16G16B16A16_UINT; case SpvImageFormatRgba8ui: return VK_FORMAT_R8G8B8A8_UINT; case SpvImageFormatR32ui: return VK_FORMAT_R32_UINT; case SpvImageFormatRgb10a2ui: return VK_FORMAT_A2R10G10B10_UINT_PACK32; case SpvImageFormatRg32ui: return VK_FORMAT_R32G32_UINT; case SpvImageFormatRg16ui: return VK_FORMAT_R16G16_UINT; case SpvImageFormatRg8ui: return VK_FORMAT_R8G8_UINT; case SpvImageFormatR16ui: return VK_FORMAT_R16_UINT; case SpvImageFormatR8ui: return VK_FORMAT_R8_UINT; case SpvImageFormatR64ui: return VK_FORMAT_R64_UINT; case SpvImageFormatR64i: return VK_FORMAT_R64_SINT; case SpvImageFormatMax: return VK_FORMAT_UNDEFINED; } return VK_FORMAT_UNDEFINED; } SamplerNumericDomain ProgramFactory::UniformTypeToSamplerNumericDomain(GLenum glType) { switch (glType) { case GL_INT_SAMPLER_1D: case GL_INT_SAMPLER_2D: case GL_INT_SAMPLER_3D: case GL_INT_SAMPLER_CUBE: case GL_INT_SAMPLER_2D_RECT: case GL_INT_SAMPLER_1D_ARRAY: case GL_INT_SAMPLER_2D_ARRAY: case GL_INT_SAMPLER_BUFFER: case GL_INT_SAMPLER_2D_MULTISAMPLE: case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: case GL_INT_SAMPLER_CUBE_MAP_ARRAY: return SamplerNumericDomain::SignedInteger; case GL_UNSIGNED_INT_SAMPLER_1D: case GL_UNSIGNED_INT_SAMPLER_2D: case GL_UNSIGNED_INT_SAMPLER_3D: case GL_UNSIGNED_INT_SAMPLER_CUBE: case GL_UNSIGNED_INT_SAMPLER_2D_RECT: case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: case GL_UNSIGNED_INT_SAMPLER_BUFFER: case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: case GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY: return SamplerNumericDomain::UnsignedInteger; case GL_SAMPLER_1D: case GL_SAMPLER_2D: case GL_SAMPLER_3D: case GL_SAMPLER_CUBE: case GL_SAMPLER_2D_RECT: case GL_SAMPLER_1D_ARRAY: case GL_SAMPLER_2D_ARRAY: case GL_SAMPLER_BUFFER: case GL_SAMPLER_2D_MULTISAMPLE: case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: case GL_SAMPLER_CUBE_MAP_ARRAY: case GL_SAMPLER_1D_SHADOW: case GL_SAMPLER_2D_SHADOW: case GL_SAMPLER_CUBE_SHADOW: case GL_SAMPLER_2D_RECT_SHADOW: case GL_SAMPLER_1D_ARRAY_SHADOW: case GL_SAMPLER_2D_ARRAY_SHADOW: case GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW: return SamplerNumericDomain::Float; default: return SamplerNumericDomain::Unknown; } } ProgramFactory::HashType ProgramFactory::ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const { XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); // We expect shader stages in program object are sorted const auto& spirvs = program.GetGeneratedSpirv(); for (const auto& spv : spirvs) { XXHASH_VERIFY(XXH64_update(m_hashState, spv.data(), spv.size() * sizeof(Uint))); } XXHASH_VERIFY(XXH64_update(m_hashState, &flags, sizeof(CompileOptionFlags))); // Only FragCoordYFlip variants bake the height in, so mixing it unconditionally would // re-key every program in the cache on a resize for no reason. if (flags & CompileOptionBit::FragCoordYFlip) { XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferHeight, sizeof(m_defaultFramebufferHeight))); } // Include UBO block bindings in hash so different binding configurations produce different entries const Uint32 blockCount = static_cast(program.GetActiveUniformBlocksCount()); XXHASH_VERIFY(XXH64_update(m_hashState, &blockCount, sizeof(blockCount))); for (Uint32 i = 0; i < blockCount; ++i) { const Uint32 binding = program.GetUniformBlockBinding(i); XXHASH_VERIFY(XXH64_update(m_hashState, &binding, sizeof(binding))); } // The transform feedback capture layout is baked into the modules by // XfbCaptureDecoratePass rather than coming from the SPIR-V, so it has to be part of // the key: two programs can share every shader and still capture differently, which // is exactly what changing the buffer mode does (glTransformFeedbackVaryings with the // same varyings but GL_SEPARATE_ATTRIBS instead of GL_INTERLEAVED_ATTRIBS). Only // hashed for a capturing compile, so nothing else changes key. if (flags & CompileOptionBit::XfbCapture) { for (const auto& varying : program.GetTransformFeedbackVaryings()) { XXHASH_VERIFY(XXH64_update(m_hashState, varying.name.data(), varying.name.size())); XXHASH_VERIFY(XXH64_update(m_hashState, &varying.bufferIndex, sizeof(varying.bufferIndex))); XXHASH_VERIFY(XXH64_update(m_hashState, &varying.offsetBytes, sizeof(varying.offsetBytes))); } const SizeT bufferCount = program.GetTransformFeedbackBufferCount(); for (SizeT i = 0; i < bufferCount; ++i) { const Uint32 stride = program.GetTransformFeedbackStride(static_cast(i)); XXHASH_VERIFY(XXH64_update(m_hashState, &stride, sizeof(stride))); } } HashType hash = XXH64_digest(m_hashState); return hash; } TextureTarget ProgramFactory::UniformTypeToTextureTarget(GLenum glType) { switch (glType) { case GL_SAMPLER_1D: case GL_INT_SAMPLER_1D: case GL_UNSIGNED_INT_SAMPLER_1D: case GL_IMAGE_1D: case GL_INT_IMAGE_1D: case GL_UNSIGNED_INT_IMAGE_1D: return TextureTarget::Texture1D; case GL_SAMPLER_3D: case GL_INT_SAMPLER_3D: case GL_UNSIGNED_INT_SAMPLER_3D: case GL_IMAGE_3D: case GL_INT_IMAGE_3D: case GL_UNSIGNED_INT_IMAGE_3D: return TextureTarget::Texture3D; case GL_SAMPLER_CUBE: case GL_SAMPLER_CUBE_SHADOW: case GL_INT_SAMPLER_CUBE: case GL_UNSIGNED_INT_SAMPLER_CUBE: case GL_IMAGE_CUBE: case GL_INT_IMAGE_CUBE: case GL_UNSIGNED_INT_IMAGE_CUBE: return TextureTarget::TextureCubeMap; case GL_SAMPLER_2D_MULTISAMPLE: case GL_INT_SAMPLER_2D_MULTISAMPLE: case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: case GL_IMAGE_2D_MULTISAMPLE: case GL_INT_IMAGE_2D_MULTISAMPLE: case GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE: return TextureTarget::Texture2DMultisample; case GL_SAMPLER_BUFFER: case GL_INT_SAMPLER_BUFFER: case GL_UNSIGNED_INT_SAMPLER_BUFFER: case GL_IMAGE_BUFFER: case GL_INT_IMAGE_BUFFER: case GL_UNSIGNED_INT_IMAGE_BUFFER: return TextureTarget::TextureBuffer; case GL_SAMPLER_1D_ARRAY: case GL_SAMPLER_1D_ARRAY_SHADOW: case GL_INT_SAMPLER_1D_ARRAY: case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: case GL_IMAGE_1D_ARRAY: case GL_INT_IMAGE_1D_ARRAY: case GL_UNSIGNED_INT_IMAGE_1D_ARRAY: return TextureTarget::Texture1DArray; case GL_SAMPLER_2D_ARRAY: case GL_SAMPLER_2D_ARRAY_SHADOW: case GL_INT_SAMPLER_2D_ARRAY: case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: case GL_IMAGE_2D_ARRAY: case GL_INT_IMAGE_2D_ARRAY: case GL_UNSIGNED_INT_IMAGE_2D_ARRAY: return TextureTarget::Texture2DArray; case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: case GL_IMAGE_2D_MULTISAMPLE_ARRAY: case GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY: case GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY: return TextureTarget::Texture2DMultisampleArray; case GL_SAMPLER_2D_RECT: case GL_SAMPLER_2D_RECT_SHADOW: case GL_INT_SAMPLER_2D_RECT: case GL_UNSIGNED_INT_SAMPLER_2D_RECT: case GL_IMAGE_2D_RECT: case GL_INT_IMAGE_2D_RECT: case GL_UNSIGNED_INT_IMAGE_2D_RECT: return TextureTarget::TextureRectangle; case GL_SAMPLER_2D: case GL_SAMPLER_2D_SHADOW: case GL_INT_SAMPLER_2D: case GL_UNSIGNED_INT_SAMPLER_2D: case GL_IMAGE_2D: case GL_INT_IMAGE_2D: case GL_UNSIGNED_INT_IMAGE_2D: default: return TextureTarget::Texture2D; } } void ProgramFactory::ReflectVertexInputs(const Vector>& shaders, const Vector>& spirv, VkProgramObject& entry) const { entry.activeVertexInputLocationMask = 0; entry.vertexInputTypes.fill(0); entry.readsBaseVertexBuiltin = false; for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) { if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Vertex) { continue; } const auto& module = spirv[moduleIndex]; if (module.empty()) { continue; } SpvReflectShaderModule reflectModule{}; const SpvReflectResult createResult = spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule); MOBILEGL_ASSERT(createResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectVertexInputs: failed to create reflection module (result=%d)", static_cast(createResult)); if (createResult != SPV_REFLECT_RESULT_SUCCESS) { continue; } entry.readsBaseVertexBuiltin = ReflectedReadsBaseVertexBuiltin(reflectModule); 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, "ProgramFactory::ReflectVertexInputs: failed to enumerate input variables (result=%d)", static_cast(reflectResult)); Vector inputs(inputCount); if (reflectResult == SPV_REFLECT_RESULT_SUCCESS && inputCount > 0) { reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, inputs.data()); MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectVertexInputs: failed to fetch input variables (result=%d)", static_cast(reflectResult)); } if (reflectResult == SPV_REFLECT_RESULT_SUCCESS) { for (auto* input : inputs) { if (input == nullptr || (input->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0) { continue; } const GLenum locationType = GetReflectInterfaceLocationType(*input); MOBILEGL_ASSERT(locationType != GL_FALSE, "ProgramFactory::ReflectVertexInputs: unsupported vertex input type at location=%u name='%s'", input->location, input->name ? input->name : ""); const Uint32 locationSpan = GetReflectInterfaceLocationSpan(*input); for (Uint32 locationOffset = 0; locationOffset < locationSpan; ++locationOffset) { const Uint32 expandedLocation = input->location + locationOffset; if (expandedLocation >= VkProgramObject::kMaxVertexInputLocations) { break; } entry.activeVertexInputLocationMask |= (1u << expandedLocation); entry.vertexInputTypes[expandedLocation] = locationType; } } } spvReflectDestroyShaderModule(&reflectModule); break; } } // Which pre-rasterization stage assigns gl_ViewportIndex is not fixed: GL 4.1 allows only the // geometry stage, ARB_shader_viewport_layer_array/GL 4.6 also the vertex and tessellation // evaluation stages. Rather than guess which one is last, every non-fragment, non-compute // module is asked - one writer anywhere means this program's draws need a multi-viewport // pipeline, and a false positive costs only a wider viewportCount. void ProgramFactory::ReflectViewportIndexUsage(const Vector>& shaders, const Vector>& spirv, VkProgramObject& entry) const { entry.writesViewportIndexBuiltin = false; for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) { if (!shaders[moduleIndex]) continue; const ShaderStage stage = shaders[moduleIndex]->GetShaderStage(); if (stage == ShaderStage::Fragment || stage == ShaderStage::Compute) continue; const auto& module = spirv[moduleIndex]; if (module.empty()) continue; SpvReflectShaderModule reflectModule{}; const SpvReflectResult createResult = spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule); if (createResult != SPV_REFLECT_RESULT_SUCCESS) { // Fail toward the wide pipeline. Missing a real gl_ViewportIndex writer would // silently collapse every viewport onto 0 (the exact bug this reflection exists // to fix); over-declaring costs one extra viewport slot on a program that never // uses it. MGLOG_E_ONCE("ProgramFactory::ReflectViewportIndexUsage: reflection failed (result=%d); assuming the " "program writes gl_ViewportIndex", static_cast(createResult)); entry.writesViewportIndexBuiltin = true; continue; } if (ReflectedWritesViewportIndexBuiltin(reflectModule)) { entry.writesViewportIndexBuiltin = true; } spvReflectDestroyShaderModule(&reflectModule); } } void ProgramFactory::ReflectFragmentOutputs(const Vector>& shaders, const Vector>& spirv, 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) { continue; } const auto& module = spirv[moduleIndex]; if (module.empty()) { continue; } SpvReflectShaderModule reflectModule{}; const SpvReflectResult createResult = spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule); MOBILEGL_ASSERT(createResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectFragmentOutputs: failed to create reflection module (result=%d)", static_cast(createResult)); if (createResult != SPV_REFLECT_RESULT_SUCCESS) { // Fail toward the exemption: stripping a genuine gl_FragDepth writer would // corrupt its depth output outright, while wrongly exempting an accumulation // pass merely reverts that one program to the pre-quirk behavior. entry.fragmentReplacesDepth = true; continue; } entry.fragmentReplacesDepth = ReflectedFragmentReplacesDepth(reflectModule); uint32_t outputCount = 0; SpvReflectResult reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, nullptr); MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectFragmentOutputs: failed to enumerate output variables (result=%d)", static_cast(reflectResult)); Vector outputs(outputCount); if (reflectResult == SPV_REFLECT_RESULT_SUCCESS && outputCount > 0) { reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, outputs.data()); MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectFragmentOutputs: failed to fetch output variables (result=%d)", static_cast(reflectResult)); } if (reflectResult == SPV_REFLECT_RESULT_SUCCESS) { for (auto* output : outputs) { if (output == nullptr || (output->decoration_flags & SPV_REFLECT_DECORATION_BUILT_IN) != 0) { continue; } const GLenum locationType = GetReflectInterfaceLocationType(*output); MOBILEGL_ASSERT(locationType != GL_FALSE, "ProgramFactory::ReflectFragmentOutputs: unsupported fragment output type at location=%u name='%s'", output->location, output->name ? output->name : ""); const Uint32 locationSpan = GetReflectInterfaceLocationSpan(*output); for (Uint32 locationOffset = 0; locationOffset < locationSpan; ++locationOffset) { const Uint32 expandedLocation = output->location + locationOffset; if (expandedLocation >= VkProgramObject::kMaxVertexInputLocations) { break; } entry.activeFragmentOutputLocationMask |= (1u << expandedLocation); entry.fragmentOutputTypes[expandedLocation] = locationType; } } } spvReflectDestroyShaderModule(&reflectModule); break; } } // How many descriptors to declare for an ARRAY of opaque uniforms (samplers, images) at one // binding. A returned count is always DECLARED in the descriptor set layout; `outDeclined` // says whether the binding can also be RESOLVED at draw time, or whether the program has to // be refused instead. // // Those are deliberately two different things. The layout must keep describing what the // shader declares even for a binding MobileGL cannot resolve: a descriptor the shader reads // and the layout omits is not a missing draw, it is an undefined descriptor access, and // lavapipe segfaults on it inside pipeline creation - in a JIT worker thread, before any // draw runs, which is why removing the binding produced a flaky crash rather than a clean // refusal. Declining is done by refusing the draw (VkProgramObject::declinedDescriptors), // not by shrinking the layout. // // Two separate things have to hold, and neither is checkable from the SPIR-V alone: // // * the count has to fit a VkDescriptorSetLayoutBinding this device will accept, and fit // the Uint16 it is stored in (65536 would narrow to 0) and the scratch the bind path // reserves from it; // * the frontend reflection has to have RESERVED that many consecutive uniform locations // for this uniform, because the per-element resolve paths address element k as // baseLocation + k. SPIRV-Reflect's `count` is the FLATTENED element count, while GL // locations follow the OUTER dimension only (ProgramObject::GetUniformArraySizeByTIndex // answers TType::getOuterArraySize()). For a one-dimensional array the two agree; for // `uniform sampler2D g[2][3]` SPIR-V says 6 where the reflection reserved 2, and // elements 2..5 would silently resolve onto whichever uniform got the next locations. // // Asking the reflection whether baseLocation and baseLocation + count - 1 are slots of the // SAME uniform tests exactly that precondition, without this code having to model how // glslang chooses to lay an array of arrays out. // // That is NOT on its own enough to start supporting the shape, though, and this check must // not be relaxed alone: the binding-qualifier unit seeding in ProgramLinkTask looks an // opaque uniform up by its name minus a trailing "[0]", so `goku[0][0]` misses the `goku` // key and every element of an array of arrays seeds texture unit 0. Resolving those elements // would then paint silently-wrong pixels with no diagnostic at all - strictly worse than // declining. The decline goes away together with the seeding fix, not before it. static Uint32 DescriptorCountForOpaqueUniformArray(const MG_State::GLState::ProgramObject& program, const String& uniformName, Uint32 binding, Int baseLocation, Uint32 reflectedCount, Uint32 maxBindings, const char* kindLabel, Bool& outDeclined) { const Uint32 count = std::max(1u, reflectedCount); if (count == 1) { return 1u; } if (count > maxBindings) { // Nothing legal to declare: the count would not fit a VkDescriptorSetLayoutBinding // this device accepts, and it would narrow badly into the Uint16 that carries it // (65536 becomes 0). Unlike the extent case below, this one CANNOT keep the layout // consistent with the shader, so refusing the draw does not fully protect it - the // driver still JITs a shader indexing past the declared count. Declaring as many as // the device allows keeps vkCreateDescriptorSetLayout succeeding and the program // inert; a device whose binding cap is smaller than a shader's array is not a // configuration MobileGL can serve at all. Needs a >maxBindings-element array to // reach (256 on desktop, ~16 on mobile). MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u has %u elements, past the %u " "this device can describe - declining the program", kindLabel, uniformName.c_str(), binding, count, maxBindings); outDeclined = true; return maxBindings; } if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, baseLocation + static_cast(count - 1u))) { MGLOG_D("ProgramFactory::ReflectLayout: %s array '%s' at binding %u spans %u descriptors but the " "reflection reserved fewer uniform locations for it (base=%d) - a multi-dimensional array " "is the usual cause, and MobileGL declines it rather than resolve elements onto a " "neighbouring uniform", kindLabel, uniformName.c_str(), binding, count, baseLocation); outDeclined = true; } return count; } void ProgramFactory::ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector>& spirv, VkProgramObject& entry) const { // Initialize layout vectors entry.bindingKinds.assign(m_maxBindings, DescriptorBindingKind::None); entry.uniformBlockIndexByBinding.assign(m_maxBindings, -1); entry.samplerNameByBinding.assign(m_maxBindings, String()); entry.samplerUniformLocationByBinding.assign(m_maxBindings, -1); entry.samplerTextureTargetByBinding.assign(m_maxBindings, TextureTarget::Texture2D); entry.samplerNumericDomainByBinding.assign(m_maxBindings, SamplerNumericDomain::Unknown); entry.storageImageFormatByBinding.assign(m_maxBindings, VK_FORMAT_UNDEFINED); entry.storageImageUsesBindingFormatByBinding.assign(m_maxBindings, false); entry.storageBlockNameByBinding.assign(m_maxBindings, String()); entry.storageBlockIndexByBinding.assign(m_maxBindings, -1); entry.globalUboBinding = -1; entry.dynamicBindings.clear(); entry.bindingDescriptorCounts.assign(m_maxBindings, 1); entry.arrayedUniformBlockIndicesByBinding.clear(); entry.declinedDescriptors = false; // Use SpvcSession (Reflection mode) to reflect all SPIR-V modules in a single pass per module for (const auto& module : spirv) { if (module.empty()) { continue; } SpvcSession session(module, SessionUsageBit::Reflection); SpvReflectShaderModule reflectModule{}; const SpvReflectResult createReflectResult = spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule); MOBILEGL_ASSERT(createReflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectLayout: failed to create reflection module (result=%d)", static_cast(createReflectResult)); // Descriptor counts per binding (UBO instance arrays reflect count > 1). UnorderedMap descriptorCountByBinding; { uint32_t countProbe = 0; if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe, nullptr) == SPV_REFLECT_RESULT_SUCCESS && countProbe > 0) { Vector probeBindings(countProbe); if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe, probeBindings.data()) == SPV_REFLECT_RESULT_SUCCESS) { for (const auto* probeBinding : probeBindings) { if (probeBinding != nullptr) { descriptorCountByBinding[probeBinding->binding] = std::max(1, probeBinding->count); } } } } } // Reflect uniform buffers auto ubos = session.GetShaderInterface(SPVC_RESOURCE_TYPE_UNIFORM_BUFFER); for (const auto& ubo : ubos) { const Uint32 binding = ubo.location; // GetShaderInterface stores binding in location field MOBILEGL_ASSERT(binding < m_maxBindings, "ProgramFactory::ReflectLayout: UBO binding %u exceeds maxBindings=%u for '%s'", binding, m_maxBindings, ubo.name.c_str()); // Check for global UBO if (std::strstr(ubo.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) { MOBILEGL_ASSERT(entry.bindingKinds[binding] == DescriptorBindingKind::None || entry.bindingKinds[binding] == DescriptorBindingKind::UniformBufferDynamic, "ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'", binding, ubo.name.c_str()); entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic; MOBILEGL_ASSERT(entry.globalUboBinding < 0 || entry.globalUboBinding == static_cast(binding), "ProgramFactory::ReflectLayout: global UBO binding mismatch (%d vs %u)", entry.globalUboBinding, binding); MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0, "ProgramFactory::ReflectLayout: global UBO shares binding %u with regular UBO index %d", binding, entry.uniformBlockIndexByBinding[binding]); entry.globalUboBinding = static_cast(binding); continue; } const auto countIt = descriptorCountByBinding.find(binding); const Uint32 descriptorCount = countIt != descriptorCountByBinding.end() ? countIt->second : 1u; if (descriptorCount <= 1) { const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str()); if (blockIndex == 0xFFFFFFFFu) { MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u", ubo.name.c_str(), binding); continue; } MOBILEGL_ASSERT(entry.bindingKinds[binding] == DescriptorBindingKind::None || entry.bindingKinds[binding] == DescriptorBindingKind::UniformBufferDynamic, "ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'", binding, ubo.name.c_str()); entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic; MOBILEGL_ASSERT(entry.globalUboBinding != static_cast(binding), "ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u", ubo.name.c_str(), binding); MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 || entry.uniformBlockIndexByBinding[binding] == static_cast(blockIndex), "ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)", binding, entry.uniformBlockIndexByBinding[binding], blockIndex); entry.uniformBlockIndexByBinding[binding] = static_cast(blockIndex); continue; } // UBO instance array: one binding, descriptorCount elements. GL exposes each // element as its own active block named "Name[i]"; map every element to its // GL block index so the descriptor write can gather per-element buffer ranges. if (descriptorCount > m_maxBindings) { MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' count %u exceeds maxBindings=%u; " "leaving binding %u unmapped", ubo.name.c_str(), descriptorCount, m_maxBindings, binding); continue; } Vector elementBlockIndices; elementBlockIndices.reserve(descriptorCount); for (Uint32 element = 0; element < descriptorCount; ++element) { String elementName = ubo.name + "[" + std::to_string(element) + "]"; Uint elementBlockIndex = program.GetUniformBlockIndex(elementName.c_str()); if (elementBlockIndex == 0xFFFFFFFFu && element == 0) { // Some frontends report the first element under the bare block name. elementBlockIndex = program.GetUniformBlockIndex(ubo.name.c_str()); } if (elementBlockIndex == 0xFFFFFFFFu) { // Degrade rather than corrupt: reuse element 0's block if we have one, // otherwise give up on the binding (same observable behavior as an // inactive block: wrong values, but no crash). MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' element %u has no active " "GL uniform block", ubo.name.c_str(), element); if (!elementBlockIndices.empty()) { elementBlockIndex = static_cast(elementBlockIndices.front()); } else { break; } } elementBlockIndices.push_back(static_cast(elementBlockIndex)); } if (elementBlockIndices.size() != descriptorCount) { MGLOG_E("ProgramFactory::ReflectLayout: skipping unresolved UBO array '%s' at binding %u", ubo.name.c_str(), binding); continue; } MOBILEGL_ASSERT(entry.bindingKinds[binding] == DescriptorBindingKind::None || entry.bindingKinds[binding] == DescriptorBindingKind::UniformBufferDynamic, "ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'", binding, ubo.name.c_str()); entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic; entry.bindingDescriptorCounts[binding] = static_cast(descriptorCount); entry.uniformBlockIndexByBinding[binding] = elementBlockIndices[0]; entry.arrayedUniformBlockIndicesByBinding[binding] = Move(elementBlockIndices); } // Reflect sampled images, storage images, samplerBuffer uniforms, and SSBOs. uint32_t reflectedBindingCount = 0; SpvReflectResult reflectResult = spvReflectEnumerateDescriptorBindings(&reflectModule, &reflectedBindingCount, nullptr); MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectLayout: failed to enumerate descriptor bindings (result=%d)", static_cast(reflectResult)); Vector reflectedBindings(reflectedBindingCount); if (reflectedBindingCount > 0) { reflectResult = spvReflectEnumerateDescriptorBindings(&reflectModule, &reflectedBindingCount, reflectedBindings.data()); MOBILEGL_ASSERT( reflectResult == SPV_REFLECT_RESULT_SUCCESS, "ProgramFactory::ReflectLayout: failed to fetch descriptor bindings (result=%d)", static_cast(reflectResult)); } for (const auto* sampler : reflectedBindings) { if (sampler == nullptr) { continue; } const auto descriptorKind = ReflectDescriptorTypeToBindingKind(sampler->descriptor_type); if (descriptorKind != DescriptorBindingKind::CombinedImageSampler && descriptorKind != DescriptorBindingKind::UniformTexelBuffer && descriptorKind != DescriptorBindingKind::StorageTexelBuffer && descriptorKind != DescriptorBindingKind::StorageImage && descriptorKind != DescriptorBindingKind::StorageBuffer) { continue; } const Uint32 binding = sampler->binding; const String uniformName = NormalizeDescriptorName(*sampler, descriptorKind); MOBILEGL_ASSERT(binding < m_maxBindings, "ProgramFactory::ReflectLayout: sampler binding %u exceeds maxBindings=%u for '%s'", binding, m_maxBindings, uniformName.c_str()); MOBILEGL_ASSERT(entry.bindingKinds[binding] == DescriptorBindingKind::None || entry.bindingKinds[binding] == descriptorKind, "ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for resource '%s'", binding, uniformName.c_str()); entry.bindingKinds[binding] = descriptorKind; if (descriptorKind == DescriptorBindingKind::StorageBuffer) { const GLuint blockIndex = GetShaderStorageBlockIndex(program, uniformName); if (blockIndex == GL_INVALID_INDEX) { MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive SSBO '%s' at binding %u", uniformName.c_str(), binding); entry.bindingKinds[binding] = DescriptorBindingKind::None; continue; } entry.storageBlockNameByBinding[binding] = uniformName; entry.storageBlockIndexByBinding[binding] = static_cast(blockIndex); // A block INSTANCE array is ONE Vulkan binding carrying `count` // descriptors, while GL assigns its elements consecutive binding points // starting at the declared one (GL 4.6 core 7.8). Recording only element 0 - // which is all this used to do - left the layout claiming descriptorCount 1, // so every element past the first read a descriptor nobody wrote and // `b[1].data.length()` answered from an unconstrained buffer instead of its // own bound range (KHR-GL43.shader_storage_buffer_object.- // advanced-unsizedArrayLength-*). // // Bounds-checked like every other array kind. The EXTENT rule differs - a // block array's elements take consecutive GL binding points rather than // consecutive uniform locations, so DescriptorCountForOpaqueUniformArray's // location test does not apply here - but the size rule is identical: this // count goes straight into a VkDescriptorSetLayoutBinding and is narrowed to // a Uint16 on the way, where 65536 would silently become 0. const Uint32 storageArrayCount = std::max(1u, sampler->count); if (storageArrayCount > m_maxBindings) { MGLOG_D("ProgramFactory::ReflectLayout: storage block array '%s' at binding %u has %u " "elements, past the %u this device can describe - declining the program", uniformName.c_str(), binding, storageArrayCount, m_maxBindings); entry.declinedDescriptors = true; entry.bindingDescriptorCounts[binding] = static_cast(m_maxBindings); continue; } entry.bindingDescriptorCounts[binding] = static_cast(storageArrayCount); continue; } const Int location = program.GetUniformLocation(uniformName); if (location < 0) { // A uniform with no location is ordinarily one GL never made active, and // dropping it is routine. An ARRAY reaching here is not routine: it is the // multi-dimensional case. `uniform sampler2D g[2][3]` arrives from // SPIRV-Reflect as one binding of 6 descriptors named "g", while the frontend // reflection keys an array of arrays by its full "[0]"-terminated spelling // ("g[0][0]"), so no base location resolves and the per-element paths have // nothing to count from. Declining is the honest answer - but it has to SAY // so at a level that survives a release build, because dropping the binding // leaves the shader reading a descriptor the layout never declared. if (sampler->count > 1) { MGLOG_E("ProgramFactory::ReflectLayout: declining '%s' at binding %u - a %u-element " "descriptor array with no frontend uniform location (a multi-dimensional array " "of samplers or images is the known cause)", uniformName.c_str(), binding, sampler->count); entry.declinedDescriptors = true; // Declared, not resolved - see DescriptorCountForOpaqueUniformArray for // why the layout keeps describing a binding the draw path will refuse. entry.bindingDescriptorCounts[binding] = static_cast(std::min(sampler->count, m_maxBindings)); continue; } entry.bindingKinds[binding] = DescriptorBindingKind::None; continue; } const GLenum uniformType = program.GetUniformType(static_cast(location)); if (descriptorKind == DescriptorBindingKind::StorageImage) { // An ARRAY of image uniforms is ONE binding carrying `count` descriptors, // and the layout has to say so. Leaving it at the default 1 declared // `uniform image2D g_image[4]` as a single-descriptor binding while the // shader indexed descriptors 1..3 of it - an out-of-bounds descriptor // access that lavapipe SIGSEGVs inside the JIT-ed shader thread rather than // reporting (KHR-GL42.shader_image_load_store.advanced-sso-simple). Unlike // a storage BLOCK array, whose elements take consecutive GL binding points // from the declared one, each element of an image array carries its own // independently assigned image unit - see ResolveStorageImageDescriptor. // Bounds- and extent-checked like the UBO array path above; see // DescriptorCountForOpaqueUniformArray for what "declined" costs and why // the reflection's reserved extent - not SPIRV-Reflect's flattened count - // is what the per-element resolve can actually address. const Uint32 imageArrayCount = DescriptorCountForOpaqueUniformArray(program, uniformName, binding, location, sampler->count, m_maxBindings, "image", entry.declinedDescriptors); entry.bindingDescriptorCounts[binding] = static_cast(imageArrayCount); const VkFormat reflectedFormat = ConvertSpirvImageFormatToVkFormat(sampler->image.image_format); VkFormat& existingFormat = entry.storageImageFormatByBinding[binding]; MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED || reflectedFormat == VK_FORMAT_UNDEFINED || existingFormat == reflectedFormat, "ProgramFactory::ReflectLayout: storage image binding %u ('%s') has " "conflicting reflected formats (%d vs %d)", binding, uniformName.c_str(), static_cast(existingFormat), static_cast(reflectedFormat)); if (existingFormat == VK_FORMAT_UNDEFINED) { existingFormat = reflectedFormat; } if (m_unformattedFloatStorageImagesEnabled && existingFormat == VK_FORMAT_UNDEFINED && IsFloatStorageImageUniformType(uniformType)) { entry.storageImageUsesBindingFormatByBinding[binding] = true; } else if (reflectedFormat != VK_FORMAT_UNDEFINED) { // A typed declaration in any stage wins for the entire binding. This is // required when another stage reaches the same image through an atomic // path and therefore could not be made formatless. entry.storageImageUsesBindingFormatByBinding[binding] = false; } } if (descriptorKind == DescriptorBindingKind::StorageTexelBuffer) { // Only the declared format is recorded, and only so the per-draw resolve can // prefer it over the one glBindImageTexture named. Everything the StorageImage // branch above does about ARRAYS is deliberately absent: an imageBuffer array // is refused outright by the array gate in RemapDescriptorBindingsForVulkan, // exactly as a samplerBuffer array is, so bindingDescriptorCounts stays at the // default 1 and the descriptor write below may take the address of a vector // element without reserving room for extra elements. const VkFormat reflectedFormat = ConvertSpirvImageFormatToVkFormat(sampler->image.image_format); VkFormat& existingFormat = entry.storageImageFormatByBinding[binding]; MOBILEGL_ASSERT(existingFormat == VK_FORMAT_UNDEFINED || reflectedFormat == VK_FORMAT_UNDEFINED || existingFormat == reflectedFormat, "ProgramFactory::ReflectLayout: storage texel buffer binding %u ('%s') " "has conflicting reflected formats (%d vs %d)", binding, uniformName.c_str(), static_cast(existingFormat), static_cast(reflectedFormat)); if (existingFormat == VK_FORMAT_UNDEFINED) { existingFormat = reflectedFormat; } } const TextureTarget target = UniformTypeToTextureTarget(uniformType); MOBILEGL_ASSERT(target != TextureTarget::Unknown, "ProgramFactory::ReflectLayout: failed to resolve texture target for '%s'", uniformName.c_str()); if (descriptorKind == DescriptorBindingKind::CombinedImageSampler) { // An ARRAY of sampler uniforms is ONE binding carrying `count` descriptors, // exactly like the image array above, and for the same reason: GLSL 4.20 // gives `layout(binding = 1) uniform sampler2D goku[4]` one declaration // spanning texture units 1..4, each element with its own glUniform1i-assigned // unit. Leaving descriptorCount at 1 declared a single-descriptor binding // while the shader indexed descriptors 1..3 of it, and the bind path wrote // only element 0 - so elements 1..N read a descriptor nobody had written // (KHR-GL42.shading_language_420pack.binding_sampler_array; lavapipe faults // inside the JIT-ed shader rather than reporting). const Uint32 samplerArrayCount = DescriptorCountForOpaqueUniformArray(program, uniformName, binding, location, sampler->count, m_maxBindings, "sampler", entry.declinedDescriptors); entry.bindingDescriptorCounts[binding] = static_cast(samplerArrayCount); const SamplerNumericDomain numericDomain = UniformTypeToSamplerNumericDomain(uniformType); MOBILEGL_ASSERT(numericDomain != SamplerNumericDomain::Unknown, "ProgramFactory::ReflectLayout: failed to resolve sampler numeric domain " "for '%s' (uniformType=0x%x)", uniformName.c_str(), uniformType); MOBILEGL_ASSERT(entry.samplerNumericDomainByBinding[binding] == SamplerNumericDomain::Unknown || entry.samplerNumericDomainByBinding[binding] == numericDomain, "ProgramFactory::ReflectLayout: sampler binding %u ('%s') has conflicting " "numeric domains (%d vs %d)", binding, uniformName.c_str(), static_cast(entry.samplerNumericDomainByBinding[binding]), static_cast(numericDomain)); entry.samplerNumericDomainByBinding[binding] = numericDomain; } MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || location < 0 || entry.samplerUniformLocationByBinding[binding] == location, "ProgramFactory::ReflectLayout: texture binding %u maps to conflicting uniform locations (%d vs %d)", binding, entry.samplerUniformLocationByBinding[binding], location); MOBILEGL_ASSERT(entry.samplerUniformLocationByBinding[binding] < 0 || entry.samplerTextureTargetByBinding[binding] == target, "ProgramFactory::ReflectLayout: texture binding %u maps to conflicting texture targets (%d vs %d)", binding, static_cast(entry.samplerTextureTargetByBinding[binding]), static_cast(target)); MOBILEGL_ASSERT(entry.samplerNameByBinding[binding].empty() || entry.samplerNameByBinding[binding] == uniformName, "ProgramFactory::ReflectLayout: texture binding %u maps to conflicting names ('%s' vs '%s')", binding, entry.samplerNameByBinding[binding].c_str(), uniformName.c_str()); if (location >= 0) { entry.samplerUniformLocationByBinding[binding] = location; } entry.samplerNameByBinding[binding] = uniformName; entry.samplerTextureTargetByBinding[binding] = target; } spvReflectDestroyShaderModule(&reflectModule); } // Build Vulkan descriptor set layout and pipeline layout from reflected binding kinds Vector bindings; bindings.reserve(m_maxBindings); for (Uint32 binding = 0; binding < m_maxBindings; ++binding) { const auto kind = entry.bindingKinds[binding]; if (kind == DescriptorBindingKind::None) { continue; } VkDescriptorSetLayoutBinding layoutBinding{}; layoutBinding.binding = binding; layoutBinding.descriptorCount = entry.bindingDescriptorCounts[binding]; layoutBinding.stageFlags = VK_SHADER_STAGE_ALL; layoutBinding.pImmutableSamplers = nullptr; if (kind == DescriptorBindingKind::UniformBufferDynamic) { layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; entry.dynamicBindings.push_back(binding); } else if (kind == DescriptorBindingKind::UniformTexelBuffer) { layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; } else if (kind == DescriptorBindingKind::StorageTexelBuffer) { layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER; } else if (kind == DescriptorBindingKind::StorageBuffer) { layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; } else if (kind == DescriptorBindingKind::StorageImage) { layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; entry.hasStorageImages = true; } else { layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; } bindings.push_back(layoutBinding); } VkDescriptorSetLayoutCreateInfo setLayoutInfo{}; setLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; setLayoutInfo.bindingCount = static_cast(bindings.size()); setLayoutInfo.pBindings = bindings.data(); VK_VERIFY(vkCreateDescriptorSetLayout(m_device, &setLayoutInfo, nullptr, &entry.descriptorSetLayout), "ProgramFactory::ReflectLayout, vkCreateDescriptorSetLayout"); VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipelineLayoutInfo.setLayoutCount = 1; pipelineLayoutInfo.pSetLayouts = &entry.descriptorSetLayout; VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayoutInfo, nullptr, &entry.pipelineLayout), "ProgramFactory::ReflectLayout, vkCreatePipelineLayout"); // Built here rather than where bindingKinds is sized: at that point the vector is only // zero-initialised and the kinds are assigned further down, so a list built there would be // empty. Ascending by construction because the index walks upward. entry.activeBindings.clear(); for (Uint32 binding = 0; binding < static_cast(entry.bindingKinds.size()); ++binding) { if (entry.bindingKinds[binding] != DescriptorBindingKind::None) { entry.activeBindings.push_back(binding); } } } void ProgramFactory::SetDefaultFramebufferHeight(Uint32 height) { if (m_defaultFramebufferHeight == height) { return; } m_defaultFramebufferHeight = height; // Both memos key on (program, flags) alone, so neither can tell the two heights apart: // drop the lookup memo, and bump the structure epoch so every caller holding a // VkProgramObject* re-runs GetOrCreateProgram and lands on the new hash. The cached // entries themselves stay - they are keyed by a hash that now includes the old height, // so they can only be reached again if that height comes back, and the frame-boundary // sweep retires them otherwise. m_lastLookup = {}; ++m_cacheStructureEpoch; } const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram( const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) { // Hashing the full SPIR-V of every stage is far too expensive to repeat per draw; // reuse the program's memoized hash while its backend state version is unchanged. // The memo keys on the flags word, which ComputeHash is no longer a pure function of: // a FragCoordYFlip variant also depends on the baked default-framebuffer height, so // that height rides in the free high half of the key. Flags occupy the low bits, and a // height cannot exceed the 16 bits a swapchain extent fits in. // // "The low bits" is load-bearing and was until now only a comment: a flag that reached // bit 16 would alias the height and two different variants would share one memo slot. static_assert(static_cast(CompileOptionBit::ZeroBaseVertex) < (1u << 16), "CompileOptionBit values must stay below bit 16: GetOrCreateProgram packs the " "default-framebuffer height into the high half of the same memo key"); const Uint memoKey = (flags & CompileOptionBit::FragCoordYFlip) ? (flags.GetRaw() | (m_defaultFramebufferHeight << 16)) : flags.GetRaw(); HashType hash = 0; if (!program.GetBackendHashMemo(memoKey, hash)) { hash = ComputeHash(program, flags); program.SetBackendHashMemo(memoKey, hash); } auto it = m_cache.find(hash); if (it != m_cache.end()) { // Every draw/dispatch funnels through this lookup (the renderer memos only // skip re-hashing, never the factory lookup), so an actively-used entry is // stamped at least once per frame boundary and can never be aged out while // any in-flight command buffer still references it. it->second.lastUsedFrame = m_frameCounter; return it->second; } // Structural change: the insert below can move every entry of this // open-addressing map, so all memoised entry pointers die here. ++m_cacheStructureEpoch; auto& entry = m_cache[hash]; entry.hash = hash; entry.lastUsedFrame = m_frameCounter; auto& shaders = program.GetAttachedShaders(); auto& spirv = program.GetGeneratedSpirv(); Vector> moduleSpirvs(spirv.size()); const ShaderStage fixupStage = PickClipFixupStage(shaders); for (SizeT i = 0; i < shaders.size(); ++i) { auto& spv = spirv[i]; if (spv.empty()) continue; // Apply position fixup if needed if (fixupStage != ShaderStage::Unknown && shaders[i] && shaders[i]->GetShaderStage() == fixupStage) { const Vector* fixupInput = &spv; Vector xfbSpirv; if ((flags & ProgramFactory::CompileOptionBit::XfbCapture) && program.GetTransformFeedbackVaryingCount() > 0) { // Decorate BEFORE the position fixup so a captured gl_Position // mirror copies the shader's own (pre-remap) value. if (TransformSpirvForXfbCapture(spv, xfbSpirv, program)) { fixupInput = &xfbSpirv; } } TransformSpirvForVulkanPositionFix(*fixupInput, moduleSpirvs[i], flags); } else { moduleSpirvs[i] = spv; } if ((flags & ProgramFactory::CompileOptionBit::ExplicitLod0Sampling) && shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Fragment) { Vector explicitLodSpirv; if (TransformSpirvForExplicitLod0Sampling(moduleSpirvs[i], explicitLodSpirv)) { moduleSpirvs[i] = Move(explicitLodSpirv); } } if ((flags & ProgramFactory::CompileOptionBit::FragCoordYFlip) && shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Fragment) { Vector fragCoordSpirv; if (TransformSpirvForFragCoordYFlip(moduleSpirvs[i], fragCoordSpirv, m_defaultFramebufferHeight)) { moduleSpirvs[i] = Move(fragCoordSpirv); } } // Vulkan's SPIR-V environment has no rectangle image dimension, so a // GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really // stored as - which addresses [0,1] where the application addressed texels. { Vector rectLoweredSpirv; if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv) && !rectLoweredSpirv.empty()) { moduleSpirvs[i] = Move(rectLoweredSpirv); } } // GL apps depend 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); decorate Position outputs Invariant so // per-pipeline compilers cannot vary the position math between passes. { Vector invariantSpirv; if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan( moduleSpirvs[i], invariantSpirv)) { moduleSpirvs[i] = std::move(invariantSpirv); } else { // 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()); } } // glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's // 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. // 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 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()); } } // The non-indexed variant of a vertex stage that reads gl_BaseVertex: GL wants zero // there, Vulkan's builtin would hand it the draw's firstVertex. Requested per draw // through CompileOptionBit::ZeroBaseVertex, so the indexed variant of the same // program keeps the native builtin and stays correct for glDrawElementsBaseVertex // and for the baseVertex word of an indexed indirect command. if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex && (flags & CompileOptionBit::ZeroBaseVertex)) { Vector zeroedSpirv; if (MG_Util::ShaderTranspiler::ShaderCompiler::ZeroBaseVertexForVulkan(moduleSpirvs[i], zeroedSpirv)) { moduleSpirvs[i] = std::move(zeroedSpirv); } else { // Failing open keeps the native builtin, which is the pre-fix behavior: // gl_BaseVertex reads firstVertex on a DrawArrays instead of zero. MGLOG_E("ProgramFactory: failed to zero gl_BaseVertex for program %u; non-indexed " "draws will read the draw's first vertex from it instead of zero", program.GetExternalIndex()); } } // A 64-bit vertex input has to arrive as its 32-bit word pair: VK_FORMAT_R64*_SFLOAT is // optional and lavapipe advertises none of them at all. The pass is unconditional so it // always agrees with the Float64 case in VertexInputStateFactory::ToVkVertexFormat, and // ReflectVertexInputs below then sees an ordinary uvec2/uvec4 input. // // Failure here is not recoverable and must not be swallowed: ToVkVertexFormat has already // committed to R32G32{,B32A32}_UINT for the attribute, so a module still declaring // `in double` would reconcile to Unknown and build a pipeline with a UINT format under a // double input - garbage with no diagnostic anywhere. if (shaders[i] && shaders[i]->GetShaderStage() == ShaderStage::Vertex) { Vector packedSpirv; const Bool packOk = MG_Util::ShaderTranspiler::ShaderCompiler::PackDoubleVertexInputsForVulkan( moduleSpirvs[i], packedSpirv); MOBILEGL_ASSERT(packOk, "ProgramFactory: 64-bit vertex input packing failed for program %u; the " "vertex-input format and the shader input type now disagree", program.GetExternalIndex()); if (packOk) { moduleSpirvs[i] = std::move(packedSpirv); } else { MGLOG_E("ProgramFactory: failed to pack 64-bit vertex inputs for program %u; " "double-typed vertex attributes will be fetched as uint32 words and not " "reinterpreted", program.GetExternalIndex()); } } // When Vulkan can legally access storage images without a statically declared // format, let GL's glBindImageTexture format select the runtime image view. This // provides desktop-driver-compatible behavior for packs such as iterationRP, whose // float image qualifier can disagree with the bound render-target format. Integer // storage images remain formatted so r32ui/r32i bit-reinterpretation paths keep the // exact descriptor format required by their shader operations. if (m_unformattedFloatStorageImagesEnabled) { Vector unformattedSpirv; if (MG_Util::ShaderTranspiler::ShaderCompiler::UseUnformattedFloatStorageImagesForVulkan( moduleSpirvs[i], unformattedSpirv)) { moduleSpirvs[i] = std::move(unformattedSpirv); } else { MGLOG_E("ProgramFactory: failed to make float storage images unformatted for program %u", program.GetExternalIndex()); } } } const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs); MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed"); for (SizeT i = 0; i < shaders.size(); ++i) { auto& moduleSpv = moduleSpirvs[i]; if (moduleSpv.empty()) continue; #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex()); #else // Final module the driver receives; also checked in the INFO-level CI/test // lanes, where the DEBUG gate above is compiled out. if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) { ValidateTransformedSpirv(moduleSpv, shaders[i]->GetShaderStage(), program.GetExternalIndex()); } #endif VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; smci.codeSize = moduleSpv.size() * sizeof(Uint); smci.pCode = moduleSpv.data(); VkShaderModule module = VK_NULL_HANDLE; VK_VERIFY(vkCreateShaderModule(m_device, &smci, nullptr, &module), "vkCreateShaderModule"); VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; ShaderStage shaderStage = shaders[i]->GetShaderStage(); stage.stage = ToVkStage(shaderStage); stage.module = module; stage.pName = "main"; entry.modules.push_back(module); entry.stages.push_back(stage); entry.stageSpirvDigests.push_back(ShaderStageSpirvDigest{ static_cast(stage.stage), static_cast(moduleSpv.size()), XXH64(moduleSpv.data(), moduleSpv.size() * sizeof(Uint), 0)}); } // Reflect and create layout as part of the program object #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG ValidateRasterizationStageInterface(shaders, moduleSpirvs, entry, program.GetExternalIndex()); #endif ReflectVertexInputs(shaders, moduleSpirvs, entry); ReflectViewportIndexUsage(shaders, moduleSpirvs, entry); ReflectFragmentOutputs(shaders, moduleSpirvs, entry); ReflectPassthroughTessControlNeed(shaders, moduleSpirvs, entry); ReflectLayout(program, moduleSpirvs, entry); // A failed remap means the modules kept glslang's per-stage auto-mapped binding numbers - // no cross-stage unification, no set->0 normalisation - so the bindings this layout // describes are not the bindings the shader reads. That has to stop the program from // drawing, and until now nothing did: the MOBILEGL_ASSERT above compiles out of every // build past DEBUG, and RemapDescriptorBindingsForVulkan's own refusal message said so at // a level an INFO build also drops. Declining is the mechanism that already exists for // "the layout and the shader disagree", so route it through that. Set AFTER ReflectLayout, // which clears the flag. if (!remapOk) { MGLOG_E("ProgramFactory::GetOrCreateProgram: declining program %u - its descriptor bindings could not " "be remapped, so the layout does not describe what the shader reads", program.GetExternalIndex()); entry.declinedDescriptors = true; } return entry; } void ProgramFactory::OnFrameBoundary() { ++m_frameCounter; // Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry // idle for more than kRetireAgeFrames frame boundaries cannot be referenced by // any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT), // so its shader modules and layouts are destroyed immediately - no deferred- // destroy machinery needed. Eviction is content-based, never tied to // glDeleteProgram: the cache is content-hash-shared across GL programs, so a // delete-driven erase could free an entry another live program still resolves. // An evicted entry self-heals - the frontend program keeps its generated // SPIR-V, so the next GetOrCreateProgram rebuilds it (this also covers the // renderer's internal blit/depth-mipmap programs). constexpr Uint64 kSweepInterval = 256; constexpr Uint64 kRetireAgeFrames = 1024; if ((m_frameCounter % kSweepInterval) != 0) { return; } for (auto it = m_cache.begin(); it != m_cache.end();) { if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) { const HashType hash = it->first; const VkDescriptorSetLayout descriptorSetLayout = it->second.descriptorSetLayout; MGLOG_D("ProgramFactory::OnFrameBoundary: evicting idle program entry hash=0x%llx", static_cast(hash)); // erase runs ~VkProgramObject (modules/layouts destroyed); notify after // so an observer never observes a half-destroyed entry through a lookup. // Observers only need the handle values to purge their keyed caches. ++m_cacheStructureEpoch; // erase moves/kills entries: memoised pointers die it = m_cache.erase(it); if (m_evictionObserver != nullptr) { m_evictionObserver->OnProgramEvicted(hash, descriptorSetLayout); } } else { ++it; } } } ProgramFactory::~ProgramFactory() { for (auto& entry : m_passthroughTessControlStages) { if (entry.second.module != VK_NULL_HANDLE) { vkDestroyShaderModule(m_device, entry.second.module, nullptr); } } } String ProgramFactory::BuildPassthroughTessControlSource(Uint32 patchVertices) { // The stage GL 4.6 core 11.2.2 describes when a program has an evaluation shader and no // control shader: "the input patch is passed through unmodified", the output patch has // as many vertices as the input one (PATCH_VERTICES), and the levels come from the // PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state. // // Those two levels default to 1.0 and are baked here as literals because // glPatchParameterfv - their only setter - is not implemented in this frontend (it is a // stub in MG_Impl/GLImpl/Exporting/Definitions.cpp). Implementing that entry point means // making the levels a parameter of this source AND of the cache key in // GetOrCreatePassthroughTessControlStage; the two must move together, so they are named // together here. // // gl_out carries gl_Position and nothing else on purpose. The evaluation stage that // reads it was linked against the VERTEX stage directly, so its input gl_PerVertex holds // exactly the built-ins that stage used, and its user-defined inputs (if any) come // straight off the vertex stage's outputs - which a control stage sitting in between // would leave unwritten. ReflectPassthroughTessControlNeed refuses those programs rather // than let this write a partial interface. // // All four outer levels and both inner levels are written unconditionally: writing a // level the evaluation stage's domain does not use is legal and ignored, and it saves // this from having to know the domain. String source = "#version 450 core\n"; source += "layout(vertices = " + std::to_string(patchVertices) + ") out;\n"; // gl_in and gl_out are redeclared to the exact gl_PerVertex the FRONTEND's linked programs // carry - gl_Position, gl_PointSize, gl_ClipDistance[1], in that order - because Vulkan // matches built-in interface blocks by their whole shape, and the two obvious spellings // are both wrong: // * narrowing the block to gl_Position alone makes the evaluation stage read a patch of // zeroes (degenerate triangles, nothing rasterized), and // * taking glslang's DEFAULT block for a standalone control stage yields FOUR members - // it appends gl_CullDistance - where a linked vertex+evaluation program has three. // PassthroughTessControlTest.MatchesTheFrontendPerVertexBlock is the latch: it links a // vertex+evaluation program through this same compiler and fails if the two shapes ever // stop agreeing, rather than letting the mismatch show up as a black frame. // // Only gl_Position is written. gl_PointSize is declared but left alone deliberately: // writing it from a tessellation stage requires the shaderTessellationAndGeometryPointSize // feature, which this renderer does not enable, so a program whose evaluation stage reads // gl_in[].gl_PointSize gets an undefined point size instead of the vertex stage's - a gap // this trades for not making every tessellated pipeline depend on an optional feature. source += "in gl_PerVertex {\n" " vec4 gl_Position;\n" " float gl_PointSize;\n" " float gl_ClipDistance[1];\n" "} gl_in[gl_MaxPatchVertices];\n"; source += "out gl_PerVertex {\n" " vec4 gl_Position;\n" " float gl_PointSize;\n" " float gl_ClipDistance[1];\n" "} gl_out[];\n"; source += "void main() {\n"; source += " gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;\n"; source += " gl_TessLevelOuter[0] = 1.0;\n"; source += " gl_TessLevelOuter[1] = 1.0;\n"; source += " gl_TessLevelOuter[2] = 1.0;\n"; source += " gl_TessLevelOuter[3] = 1.0;\n"; source += " gl_TessLevelInner[0] = 1.0;\n"; source += " gl_TessLevelInner[1] = 1.0;\n"; source += "}\n"; return source; } VkPipelineShaderStageCreateInfo ProgramFactory::GetOrCreatePassthroughTessControlStage(Uint32 patchVertices) { // A cached VK_NULL_HANDLE is a remembered failure, not a miss: returning it keeps a // generator that cannot compile from re-running glslang on every draw. const auto cached = m_passthroughTessControlStages.find(patchVertices); if (cached != m_passthroughTessControlStages.end()) { return cached->second; } VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; stage.stage = VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT; stage.module = VK_NULL_HANDLE; stage.pName = "main"; using namespace MG_Util::ShaderTranspiler; const String source = BuildPassthroughTessControlSource(patchVertices); // Same compile configuration as every other stage of every other program: this runs on // the GL thread (the draw path), so the live compile env is the right one, and flags=0 // is the Vulkan-targeting form (CompileForOpenGL is what the GLES backend adds). const SharedPtr& env = GetCurrentCompileEnv(); ShaderAttrib shaderAttrib{.shaderType = GL_TESS_CONTROL_SHADER, .sourceStr = source, .flags = 0, .env = env.get()}; auto compiled = ShaderCompiler::CompileShader(shaderAttrib); if (!compiled) { MGLOG_E("ProgramFactory: could not compile the pass-through tessellation control stage for " "patchVertices=%u; a program with an evaluation stage and no control stage cannot draw. %s", patchVertices, compiled.error().log.c_str()); m_passthroughTessControlStages.emplace(patchVertices, stage); return stage; } ProgramAttrib programAttrib{}; programAttrib.shaders.push_back(compiled.value()); auto linked = ShaderCompiler::LinkProgram(programAttrib); if (!linked) { MGLOG_E("ProgramFactory: could not link the pass-through tessellation control stage for " "patchVertices=%u. %s", patchVertices, linked.error().log.c_str()); m_passthroughTessControlStages.emplace(patchVertices, stage); return stage; } ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_TESS_CONTROL_SHADER}, .program = *linked.value()}; auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); if (!binary || binary.value().empty() || binary.value().front().empty()) { MGLOG_E("ProgramFactory: could not generate SPIR-V for the pass-through tessellation control stage " "for patchVertices=%u", patchVertices); m_passthroughTessControlStages.emplace(patchVertices, stage); return stage; } const Vector& spirv = binary.value().front(); #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0); #else if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvValidationEnabled()) { ValidateTransformedSpirv(spirv, ShaderStage::TessControl, 0); } #endif VkShaderModuleCreateInfo smci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; smci.codeSize = spirv.size() * sizeof(Uint); smci.pCode = spirv.data(); VkShaderModule module = VK_NULL_HANDLE; const VkResult result = vkCreateShaderModule(m_device, &smci, nullptr, &module); if (result != VK_SUCCESS) { MGLOG_E("ProgramFactory: vkCreateShaderModule failed (%d) for the pass-through tessellation control " "stage for patchVertices=%u", static_cast(result), patchVertices); m_passthroughTessControlStages.emplace(patchVertices, stage); return stage; } stage.module = module; MGLOG_D("ProgramFactory: built the pass-through tessellation control stage for patchVertices=%u " "(GL 4.6 11.2.2; Vulkan has no fixed-function equivalent)", patchVertices); m_passthroughTessControlStages.emplace(patchVertices, stage); return stage; } void ProgramFactory::ReflectPassthroughTessControlNeed( const Vector>& shaders, const Vector>& spirv, VkProgramObject& entry) const { entry.needsPassthroughTessControl = false; entry.passthroughTessControlEmulatable = false; Bool hasTessEval = false; Bool hasTessControl = false; SizeT tessEvalModuleIndex = 0; for (SizeT i = 0; i < shaders.size(); ++i) { if (!shaders[i]) continue; const auto stage = shaders[i]->GetShaderStage(); if (stage == ShaderStage::TessControl) hasTessControl = true; if (stage == ShaderStage::TessEval) { hasTessEval = true; tessEvalModuleIndex = i; } } if (!hasTessEval || hasTessControl) return; entry.needsPassthroughTessControl = true; if (tessEvalModuleIndex >= spirv.size() || spirv[tessEvalModuleIndex].empty()) return; const auto& module = spirv[tessEvalModuleIndex]; SpvReflectShaderModule reflectModule{}; const SpvReflectResult createResult = spvReflectCreateShaderModule(module.size() * sizeof(Uint), module.data(), &reflectModule); if (createResult != SPV_REFLECT_RESULT_SUCCESS) { MGLOG_E("ProgramFactory::ReflectPassthroughTessControlNeed: reflection failed (result=%d); the " "evaluation stage's inputs are unknown, so the pass-through is not offered", static_cast(createResult)); return; } uint32_t inputCount = 0; SpvReflectResult reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, nullptr); Vector inputs(inputCount); if (reflectResult == SPV_REFLECT_RESULT_SUCCESS && inputCount > 0) { reflectResult = spvReflectEnumerateInputVariables(&reflectModule, &inputCount, inputs.data()); } if (reflectResult != SPV_REFLECT_RESULT_SUCCESS) { spvReflectDestroyShaderModule(&reflectModule); return; } // The question is only ever "does this stage read anything a control stage would have to // forward", and the answer is: does it have a LOCATION. A located input is a user-defined // varying (or a per-patch input), which the vertex stage writes today and would stop // reaching once a control stage sits in between - the pass-through carries gl_Position and // nothing else, so such a program is declined instead of being handed undefined values. // Everything without a location is a built-in: gl_in, gl_TessCoord, gl_PatchVerticesIn, // gl_PrimitiveID, gl_TessLevel*, all either forwarded or generated for the evaluation // stage by the tessellator itself. // // This deliberately does NOT judge on SpvReflectInterfaceVariable::built_in. gl_in is an // array of interface blocks, and for those SPIRV-Reflect reports built_in == -1 on the // block AND leaves every member's built_in at 0 - which is SpvBuiltInPosition, so a // member walk reads "Position, Position, Position" for a {Position, PointSize, // ClipDistance} block and would accept anything on the strength of parse garbage. The // location, by contrast, is decorated on the OpVariable and is what SPIRV-Reflect reads // straight through. constexpr Uint32 kNoLocation = 0xFFFFFFFFu; Bool emulatable = true; for (auto* input : inputs) { if (input == nullptr) continue; if (input->location == kNoLocation) continue; MGLOG_E("ProgramFactory: a tessellation evaluation stage with no control stage reads the " "user-defined input '%s' at location=%u; a synthesized control stage cannot forward it, so " "this program's draws are declined rather than fed an undefined varying", input->name != nullptr ? input->name : "", input->location); emulatable = false; break; } spvReflectDestroyShaderModule(&reflectModule); entry.passthroughTessControlEmulatable = emulatable; } } // namespace MobileGL::MG_Backend::DirectVulkan