// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.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 "PipelineFactory.h"

#include <algorithm>

namespace MobileGL::MG_Backend::DirectVulkan {
    static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
        switch (topology) {
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_POINT_LIST)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_LIST)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_STRIP)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY)
        ENUM_STR_CASE(VK_PRIMITIVE_TOPOLOGY_PATCH_LIST)
        default:
            return "VK_PRIMITIVE_TOPOLOGY_UNKNOWN";
        }
    }

    static const char* SampleCountToString(VkSampleCountFlagBits sampleCount) {
        switch (sampleCount) {
        ENUM_STR_CASE(VK_SAMPLE_COUNT_1_BIT)
        ENUM_STR_CASE(VK_SAMPLE_COUNT_2_BIT)
        ENUM_STR_CASE(VK_SAMPLE_COUNT_4_BIT)
        ENUM_STR_CASE(VK_SAMPLE_COUNT_8_BIT)
        ENUM_STR_CASE(VK_SAMPLE_COUNT_16_BIT)
        ENUM_STR_CASE(VK_SAMPLE_COUNT_32_BIT)
        ENUM_STR_CASE(VK_SAMPLE_COUNT_64_BIT)
        default:
            return "VK_SAMPLE_COUNT_UNKNOWN";
        }
    }

    static const char* CullModeToString(VkCullModeFlags cullMode) {
        switch (cullMode) {
        case VK_CULL_MODE_NONE:
            return "VK_CULL_MODE_NONE";
        case VK_CULL_MODE_FRONT_BIT:
            return "VK_CULL_MODE_FRONT_BIT";
        case VK_CULL_MODE_BACK_BIT:
            return "VK_CULL_MODE_BACK_BIT";
        case VK_CULL_MODE_FRONT_AND_BACK:
            return "VK_CULL_MODE_FRONT_AND_BACK";
        default:
            return "VK_CULL_MODE_UNKNOWN";
        }
    }

    static const char* CompareOpToString(VkCompareOp compareOp) {
        switch (compareOp) {
        ENUM_STR_CASE(VK_COMPARE_OP_NEVER)
        ENUM_STR_CASE(VK_COMPARE_OP_LESS)
        ENUM_STR_CASE(VK_COMPARE_OP_EQUAL)
        ENUM_STR_CASE(VK_COMPARE_OP_LESS_OR_EQUAL)
        ENUM_STR_CASE(VK_COMPARE_OP_GREATER)
        ENUM_STR_CASE(VK_COMPARE_OP_NOT_EQUAL)
        ENUM_STR_CASE(VK_COMPARE_OP_GREATER_OR_EQUAL)
        ENUM_STR_CASE(VK_COMPARE_OP_ALWAYS)
        default:
            return "VK_COMPARE_OP_UNKNOWN";
        }
    }

    static const char* LogicOpToString(VkLogicOp logicOp) {
        switch (logicOp) {
        ENUM_STR_CASE(VK_LOGIC_OP_CLEAR)
        ENUM_STR_CASE(VK_LOGIC_OP_AND)
        ENUM_STR_CASE(VK_LOGIC_OP_AND_REVERSE)
        ENUM_STR_CASE(VK_LOGIC_OP_COPY)
        ENUM_STR_CASE(VK_LOGIC_OP_AND_INVERTED)
        ENUM_STR_CASE(VK_LOGIC_OP_NO_OP)
        ENUM_STR_CASE(VK_LOGIC_OP_XOR)
        ENUM_STR_CASE(VK_LOGIC_OP_OR)
        ENUM_STR_CASE(VK_LOGIC_OP_NOR)
        ENUM_STR_CASE(VK_LOGIC_OP_EQUIVALENT)
        ENUM_STR_CASE(VK_LOGIC_OP_INVERT)
        ENUM_STR_CASE(VK_LOGIC_OP_OR_REVERSE)
        ENUM_STR_CASE(VK_LOGIC_OP_COPY_INVERTED)
        ENUM_STR_CASE(VK_LOGIC_OP_OR_INVERTED)
        ENUM_STR_CASE(VK_LOGIC_OP_NAND)
        ENUM_STR_CASE(VK_LOGIC_OP_SET)
        default:
            return "VK_LOGIC_OP_UNKNOWN";
        }
    }

    PipelineFactory::PipelineFactory(VkDevice device, const VulkanRendererConfig& config):
        m_device(device), m_config(config) {
        MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "PipelineFactory: device is null");

        if (m_config.DisablePipelineCache) {
            MGLOG_I("DirectVulkan: pipeline cache disabled");
            return;
        }

        VkPipelineCacheCreateInfo pipelineCacheInfo{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO};
        VK_VERIFY(vkCreatePipelineCache(m_device, &pipelineCacheInfo, nullptr, &m_pipelineCache),
                  "vkCreatePipelineCache");
    }

    // Must be called once, before any pipeline is created: the flag is not part of the
    // pipeline hash, so flipping it mid-life would serve cached pipelines built under the
    // old value.
    void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
        s_suppressBlendedDepthWrite = enabled;
    }

    Bool PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
                                                                   Uint32 vendorId) {
        static constexpr Uint32 kVendorIdQualcomm = 0x5143;
        switch (quirkOverride) {
        case MG_Config::QuirkOverride::ForceOn:
            return true;
        case MG_Config::QuirkOverride::ForceOff:
            return false;
        case MG_Config::QuirkOverride::Auto:
        default:
            return vendorId == kVendorIdQualcomm;
        }
    }

    namespace {
        // MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass
        // (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing
        // depth for its equality chain). MIN/MAX ignore blend factors per the Vulkan spec.
        //
        // Deliberately the ONLY shape stripped. A quirk should touch as little unrelated
        // content as possible, and a trace sweep of every fixture showed the wider
        // alternatives all cost more than they fix:
        //   - additive ONE+ONE with a depth write matched zero draws of the 26.3 chain
        //     (its transmittance/accumulate passes disable depth writes themselves) - the
        //     only real content it caught was harmless additive glow effects (Create);
        //   - sorted-transparency "over" blends (SRC_ALPHA-style) are order-dependent,
        //     drawn once per surface, and rely on their depth writes for occlusion;
        //   - separate-alpha accumulation over an over-blending color channel has no
        //     known pairing with a depth-equality chain (color channel only, see tests).
        // If a future workload pairs another blend shape with an equality chain, widen
        // this with that evidence in hand rather than pre-emptively.
        Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
            return attachment.colorBlendOp == VK_BLEND_OP_MIN ||
                   attachment.colorBlendOp == VK_BLEND_OP_MAX;
        }
    } // namespace

    Bool PipelineFactory::ShouldSuppressDepthWrite(const PipelineCreatePayload& payload) {
        if (!payload.depthWriteEnable) {
            return false;
        }
        // A shader that assigns gl_FragDepth supplies depth itself rather than taking the
        // pipeline's interpolated Z, so a driver that varies the vertex position math
        // between pipelines cannot desynchronize it. (A gl_FragDepth = gl_FragCoord.z
        // passthrough is the exception that stays exposed; no known content pairs one with
        // an equality chain, and 26.3's composite is a genuine computed-depth writer.)
        if (payload.fragmentReplacesDepth) {
            return false;
        }
        for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
            const VkPipelineColorBlendAttachmentState& attachment = payload.colorBlendAttachments[i];
            if (attachment.blendEnable != VK_TRUE) {
                continue;
            }
            // All color writes masked: blending is moot (depth-prepass pattern that left
            // GL_BLEND enabled); stripping the depth write would delete the whole prepass.
            if (attachment.colorWriteMask == 0) {
                continue;
            }
            // Any attachment qualifies, not just attachment 0: the 26.3 transmittance pass
            // accumulates into a 2-target MRT and must stay stripped.
            if (IsAccumulationBlend(attachment)) {
                return true;
            }
        }
        return false;
    }

    PipelineFactory::~PipelineFactory() {
        DestroyAll();
        if (m_pipelineCache != VK_NULL_HANDLE) {
            vkDestroyPipelineCache(m_device, m_pipelineCache, nullptr);
            m_pipelineCache = VK_NULL_HANDLE;
        }
    }

    PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const {
        XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
        XXHASH_VERIFY(
            XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
                                   sizeof(payload.passthroughTessControlKey)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
        XXHASH_VERIFY(
            XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
        XXHASH_VERIFY(
            XXH64_update(m_hashState, &payload.rasterizerDiscardEnable, sizeof(payload.rasterizerDiscardEnable)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOpEnable, sizeof(payload.logicOpEnable)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.stencilTestEnable, sizeof(payload.stencilTestEnable)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthCompareOp, sizeof(payload.depthCompareOp)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.logicOp, sizeof(payload.logicOp)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilFailOp, sizeof(payload.frontStencilFailOp)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontStencilPassOp, sizeof(payload.frontStencilPassOp)));
        XXHASH_VERIFY(
            XXH64_update(m_hashState, &payload.frontStencilDepthFailOp, sizeof(payload.frontStencilDepthFailOp)));
        XXHASH_VERIFY(
            XXH64_update(m_hashState, &payload.frontStencilCompareOp, sizeof(payload.frontStencilCompareOp)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilFailOp, sizeof(payload.backStencilFailOp)));
        XXHASH_VERIFY(XXH64_update(m_hashState, &payload.backStencilPassOp, sizeof(payload.backStencilPassOp)));
        XXHASH_VERIFY(
            XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
        XXHASH_VERIFY(
            XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
        XXHASH_VERIFY(
            XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
        if (payload.colorAttachmentCount > 0) {
            XXHASH_VERIFY(XXH64_update(
                m_hashState,
                payload.colorBlendAttachments.data(),
                sizeof(payload.colorBlendAttachments[0]) * payload.colorAttachmentCount));
        }
        return XXH64_digest(m_hashState);
    }

    VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) {
        const HashType hash = ComputeHash(payload);
        auto it = m_cache.find(hash);
        if (it != m_cache.end()) {
            it->second.lastUsedFrame = m_frameCounter;
            return it->second.pipeline;
        }

        VkPipeline pipeline = CreatePipeline(payload);
        // A failed creation must never be memoized. Caching VK_NULL_HANDLE served the null back for
        // the rest of the process, so one transient driver rejection turned every later draw with
        // the same state into a vkCmdBindPipeline(VK_NULL_HANDLE) - the SIGSEGV behind 9 of the 15
        // CTS process deaths. Retrying costs one failed vkCreateGraphicsPipelines per draw, which
        // is the correct price for a broken pipeline and is bounded by the draw itself being
        // skipped.
        if (pipeline == VK_NULL_HANDLE) {
            // Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
            // assembled and the driver refused is a broken invariant, not an expected failure,
            // so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
            // Log.h ordering fix made MGLOG_E live in INFO builds.
            MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
                    "programHash=0x%llx; not caching the failure",
                    static_cast<unsigned long long>(hash),
                    static_cast<unsigned long long>(payload.programHash));
            return VK_NULL_HANDLE;
        }
        m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
                                                 m_frameCounter});
        return pipeline;
    }

    void PipelineFactory::DestroyAll() {
        for (auto& pair : m_cache) {
            if (pair.second.pipeline != VK_NULL_HANDLE) {
                vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
            }
        }
        m_cache.clear();
    }

    Uint32 PipelineFactory::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 immediate vkDestroyPipeline is safe. The caller must drop its "last
        // pipeline" memo when this returns non-zero: the memo can return a cached
        // handle without touching this cache, so an evicted pipeline may still be
        // memoized (present-less flush loops never reset the memo per frame).
        constexpr Uint64 kSweepInterval = 256;
        constexpr Uint64 kRetireAgeFrames = 1024;
        if ((m_frameCounter % kSweepInterval) != 0) {
            return 0;
        }

        Uint32 evicted = 0;
        for (auto it = m_cache.begin(); it != m_cache.end();) {
            if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
                if (it->second.pipeline != VK_NULL_HANDLE) {
                    vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
                }
                it = m_cache.erase(it);
                ++evicted;
            } else {
                ++it;
            }
        }
        if (evicted > 0) {
            MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
                    m_cache.size());
        }
        return evicted;
    }

    Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
        if (renderPasses.empty() || m_cache.empty()) {
            return 0;
        }
        // Sorted-batch membership test keeps a mass eviction (shader-pack switch,
        // dimension exit) at one O(cache * log batch) scan instead of one full scan
        // per dying pass.
        Vector<VkRenderPass> sortedPasses = renderPasses;
        std::sort(sortedPasses.begin(), sortedPasses.end());
        Uint32 evicted = 0;
        for (auto it = m_cache.begin(); it != m_cache.end();) {
            if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
                if (it->second.pipeline != VK_NULL_HANDLE) {
                    vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
                }
                it = m_cache.erase(it);
                ++evicted;
            } else {
                ++it;
            }
        }
        if (evicted > 0) {
            MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
                    evicted, sortedPasses.size());
        }
        return evicted;
    }

    Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
        Uint32 evicted = 0;
        for (auto it = m_cache.begin(); it != m_cache.end();) {
            if (it->second.programHash == programHash) {
                if (it->second.pipeline != VK_NULL_HANDLE) {
                    vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
                }
                it = m_cache.erase(it);
                ++evicted;
            } else {
                ++it;
            }
        }
        if (evicted > 0) {
            MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
                    evicted, static_cast<unsigned long long>(programHash));
        }
        return evicted;
    }

    VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
        MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
        MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
        MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null");
        MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null");
        MOBILEGL_ASSERT(payload.colorAttachmentCount <= PipelineCreatePayload::kMaxColorAttachments,
                "PipelineFactory: colorAttachmentCount=%u is unexpectedly large",
                payload.colorAttachmentCount);
        MGLOG_D("PipelineFactory::CreatePipeline: programHash=0x%llx vertexInputHash=0x%llx colorAttachmentCount=%u subpass=%u",
            static_cast<unsigned long long>(payload.programHash),
            static_cast<unsigned long long>(payload.vertexInputHash),
            payload.colorAttachmentCount,
            payload.subpass);

        static constexpr VkDynamicState kDynamicStates[] = {
            VK_DYNAMIC_STATE_VIEWPORT,
            VK_DYNAMIC_STATE_SCISSOR,
            VK_DYNAMIC_STATE_BLEND_CONSTANTS,
            VK_DYNAMIC_STATE_DEPTH_BIAS,
            VK_DYNAMIC_STATE_LINE_WIDTH,
            VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK,
            VK_DYNAMIC_STATE_STENCIL_WRITE_MASK,
            VK_DYNAMIC_STATE_STENCIL_REFERENCE
        };

        VkPipelineDynamicStateCreateInfo dynamicState{};
        dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
        dynamicState.dynamicStateCount = static_cast<uint32_t>(std::size(kDynamicStates));
        dynamicState.pDynamicStates = kDynamicStates;

        VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
        ia.topology = payload.topology;
        ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;

        // Only a patch topology has a tessellation stage to configure; leaving the pointer null
        // otherwise is what the spec expects.
        VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO};
        tessellation.patchControlPoints = payload.patchControlPoints;

        VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
        // Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
        // requires viewportCount == scissorCount whenever both are dynamic
        // (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
        // clamped this to the device's multiViewport capability.
        vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
        vpci.scissorCount = vpci.viewportCount;

        VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
        raster.polygonMode = payload.polygonMode;
        raster.cullMode = payload.cullMode;
        raster.frontFace = payload.frontFace;
        raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
        raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
        raster.lineWidth = 1.0f;
        // Only chain the struct when the mode is not Vulkan's implicit default: a device without
        // VK_EXT_provoking_vertex enabled must never see this pNext entry, and the renderer's
        // selector already collapses to FIRST in exactly that case - so a device without the
        // extension produces a byte-identical VkGraphicsPipelineCreateInfo to before.
        VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState{
            VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT};
        if (payload.provokingVertexMode != VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
            provokingVertexState.provokingVertexMode = payload.provokingVertexMode;
            provokingVertexState.pNext = raster.pNext;
            raster.pNext = &provokingVertexState;
        }

        VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
        ms.rasterizationSamples = payload.rasterizationSamples;
        ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
        // Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
        // struct's bytes match the hash the payload was keyed by.
        ms.minSampleShading = payload.minSampleShading;
        // GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
        // now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
        // The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
        ms.pSampleMask = payload.sampleMask;

        VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
        depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
        depthStencil.depthWriteEnable = payload.depthWriteEnable ? VK_TRUE : VK_FALSE;
        depthStencil.depthCompareOp = payload.depthCompareOp;
        depthStencil.depthBoundsTestEnable = VK_FALSE;
        depthStencil.stencilTestEnable = payload.stencilTestEnable ? VK_TRUE : VK_FALSE;
        if (payload.stencilTestEnable) {
            depthStencil.front.failOp = payload.frontStencilFailOp;
            depthStencil.front.passOp = payload.frontStencilPassOp;
            depthStencil.front.depthFailOp = payload.frontStencilDepthFailOp;
            depthStencil.front.compareOp = payload.frontStencilCompareOp;
            depthStencil.front.compareMask = 0xffffffffu;
            depthStencil.front.writeMask = 0xffffffffu;
            depthStencil.front.reference = 0;
            depthStencil.back.failOp = payload.backStencilFailOp;
            depthStencil.back.passOp = payload.backStencilPassOp;
            depthStencil.back.depthFailOp = payload.backStencilDepthFailOp;
            depthStencil.back.compareOp = payload.backStencilCompareOp;
            depthStencil.back.compareMask = 0xffffffffu;
            depthStencil.back.writeMask = 0xffffffffu;
            depthStencil.back.reference = 0;
        }

        Vector<VkPipelineColorBlendAttachmentState> colorAttachments(payload.colorAttachmentCount);
        for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
            colorAttachments[i] = payload.colorBlendAttachments[i];
        }
        // Suppress depth writes on accumulation-blended pipelines when the active driver
        // cannot keep vertex positions invariant across the pipelines of a multi-pass
        // depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed
        // in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water),
        // gl_FragDepth writers, and masked-out attachments keep their depth writes.
        // This bakes the decision into the pipeline, which only works because depth write is
        // static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates
        // would let the record-time value override it and silently disable the quirk.
        if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) {
            depthStencil.depthWriteEnable = VK_FALSE;
        }
        VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
        blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
        blend.logicOp = payload.logicOp;
        blend.attachmentCount = payload.colorAttachmentCount;
        blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();

        // A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
        // 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
        // VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
        // neither - so the renderer synthesizes the pass-through GL describes and hands it in
        // here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
        //
        // The refusal below is what keeps the half-tessellated shape away from the driver when
        // there is no synthesized stage to add - because Mali does not reject it, it dereferences
        // null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
        // 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
        // wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
        // takes: the draw is skipped, nothing is memoised, and the process survives.
        const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
        Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
        if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
            stagesWithPassthrough = *payload.stages;
            stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
            effectiveStages = &stagesWithPassthrough;
        }
        {
            VkShaderStageFlags stagesPresent = 0;
            for (const auto& stageInfo : *effectiveStages) {
                stagesPresent |= stageInfo.stage;
            }
            const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
            const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
            if (hasTessControl != hasTessEval) {
                // Latched, and the latch is the point: a failed creation is deliberately never
                // memoised (see GetOrCreatePipeline), so a program in this state re-enters here
                // once per draw, every frame - and a refusal diagnostic that repeats per draw is
                // noise, not a diagnostic. One line names the program; the draws it explains are
                // all the same draw.
                static Bool s_warnedHalfTessellatedPipeline = false;
                if (!s_warnedHalfTessellatedPipeline) {
                    s_warnedHalfTessellatedPipeline = true;
                    MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
                            "no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
                            "patchControlPoints=%u. Its draws are skipped; logged once.",
                            hasTessEval ? "an evaluation" : "a control",
                            hasTessEval ? "control" : "evaluation",
                            static_cast<unsigned long long>(payload.programHash),
                            payload.patchControlPoints);
                }
                return VK_NULL_HANDLE;
            }
        }

        VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
        gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
        gpi.pStages = effectiveStages->data();
        gpi.pVertexInputState = payload.vertexInputState;
        gpi.pInputAssemblyState = &ia;
        gpi.pTessellationState =
            payload.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST ? &tessellation : nullptr;
        gpi.pViewportState = &vpci;
        gpi.pRasterizationState = &raster;
        gpi.pMultisampleState = &ms;
        gpi.pDepthStencilState = &depthStencil;
        gpi.pColorBlendState = &blend;
        gpi.pDynamicState = &dynamicState;
        gpi.layout = payload.pipelineLayout;
        gpi.renderPass = payload.renderPass;
        gpi.subpass = payload.subpass;

        VkPipeline pipeline = VK_NULL_HANDLE;
        const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
        // Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
        // pipeline MobileGL assembled is a should-never-happen state, and the driver's own
        // answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
        // diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
        // does not apply: while this is reachable it should keep saying so on every draw.
        // GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
        // repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
        if (result != VK_SUCCESS) {
            MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
                    VkResultToString(result),
                    result,
                    static_cast<unsigned long long>(payload.programHash),
                    static_cast<unsigned long long>(payload.vertexInputHash),
                    gpi.stageCount,
                    PrimitiveTopologyToString(payload.topology),
                    payload.topology,
                    payload.colorAttachmentCount,
                    SampleCountToString(payload.rasterizationSamples),
                    payload.rasterizationSamples,
                    payload.subpass);
            MGLOG_F("PipelineFactory::CreatePipeline state: cullMode=%s(0x%x) frontFace=%d depthTest=%d depthWrite=%d depthCompare=%s(%d) depthBias=%d rasterizerDiscard=%d stencilTest=%d logicOpEnable=%d logicOp=%s(%d)",
                    CullModeToString(payload.cullMode),
                    static_cast<Uint32>(payload.cullMode),
                    payload.frontFace,
                    payload.depthTestEnable ? 1 : 0,
                    payload.depthWriteEnable ? 1 : 0,
                    CompareOpToString(payload.depthCompareOp),
                    payload.depthCompareOp,
                    payload.depthBiasEnable ? 1 : 0,
                    payload.rasterizerDiscardEnable ? 1 : 0,
                    payload.stencilTestEnable ? 1 : 0,
                    payload.logicOpEnable ? 1 : 0,
                    LogicOpToString(payload.logicOp),
                    payload.logicOp);
            MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
                    payload.vertexInputState->vertexBindingDescriptionCount,
                    payload.vertexInputState->vertexAttributeDescriptionCount);
            // The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
            // way to work out WHICH shader it choked on (the open sampler-array-in-struct
            // investigation) is to name the modules. MGLOG_I, not _D: this is part of a
            // should-never-happen report and must survive in the INFO-level builds that CTS
            // actually runs against, alongside the MGLOG_F lines above.
            if (payload.stageSpirvDigests) {
                for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
                    const auto& digest = (*payload.stageSpirvDigests)[i];
                    MGLOG_I("PipelineFactory::CreatePipeline spirv[%zu]: stage=0x%x words=%u bytes=%zu "
                            "hash=0x%llx",
                            i, digest.stage, digest.wordCount,
                            static_cast<SizeT>(digest.wordCount) * sizeof(Uint32),
                            static_cast<unsigned long long>(digest.hash));
                }
            } else {
                MGLOG_I("PipelineFactory::CreatePipeline: no SPIR-V digests attached to the payload");
            }
            if (payload.stages) {
                for (SizeT i = 0; i < payload.stages->size(); ++i) {
                    const auto& stage = (*payload.stages)[i];
                    // VkShaderModule is a non-dispatchable handle: a pointer on 64-bit but a
                    // plain uint64_t on 32-bit ABIs, where a cast to const void* is ill-formed
                    // (broke the armeabi-v7a build). Print it as the 64-bit value it is.
                    MGLOG_I("PipelineFactory::CreatePipeline stage[%zu]: stage=0x%x module=0x%llx entry=%s "
                            "specialization=%d",
                            i, static_cast<Uint32>(stage.stage),
                            static_cast<unsigned long long>(reinterpret_cast<Uint64>(stage.module)),
                            stage.pName ? stage.pName : "(null)", stage.pSpecializationInfo ? 1 : 0);
                }
            }
            for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
                const auto& attachment = payload.colorBlendAttachments[i];
                MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
                        i,
                        attachment.blendEnable == VK_TRUE ? 1 : 0,
                        static_cast<Uint32>(attachment.colorWriteMask),
                        attachment.srcColorBlendFactor,
                        attachment.dstColorBlendFactor,
                        attachment.colorBlendOp,
                        attachment.srcAlphaBlendFactor,
                        attachment.dstAlphaBlendFactor,
                        attachment.alphaBlendOp);
            }
        }
        VK_VERIFY(result, "vkCreateGraphicsPipelines");
        return pipeline;
    }
} // namespace MobileGL::MG_Backend::DirectVulkan
