mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 06:38:31 +09:00
Vulkan's built-in convention is "provoking vertex first"; GL's default is LAST_VERTEX_CONVENTION, and GL derives both flat shading and the transform feedback vertex order from it. DirectVulkan had no way to say so, which is why direct_state_access.queries_functional failed on a value with nothing in its log - the primitives came back counted against a strip recorded in the wrong vertex order. VK_EXT_provoking_vertex is now enabled when present, and the mode is a hashed field of the pipeline payload rather than dynamic state, because it is baked into VkPipelineRasterizationStateCreateInfo: two draws differing only in it must not collide on one cached VkPipeline, or whichever mode built first would stick for the rest of the frame. The pNext is chained only when the mode is not Vulkan's default, so a device without the extension produces a byte-identical VkGraphicsPipelineCreateInfo to before. Two carve-outs, both measured rather than reasoned: A geometry shader already emits its triangles in GL's vertex order, so asking for LAST rotates them a second time and transform_feedback.geometry reads back the wrong vertices. The mode is one pipeline bit and the input-assembler path wants the opposite, so the two cannot both be satisfied: a program that runs a geometry shader and captures transform feedback keeps Vulkan's own convention. That test is read off the program's own shader list, not programObj.rasterizationProducerStage - the latter is filled by the clip-fixup analysis, which does not run for every program and reads Unknown for exactly the programs this guard exists to catch. Both halves are link-time facts folded into programObj.hash, so no pipeline memo can hand back one built for the other mode; keying on IsTransformFeedbackActive() instead would be a live bug, since neither memo key moves on glBeginTransformFeedback. transformFeedbackPreservesProvokingVertex is deliberately not requested. It buys nothing here - the capture order queries_functional needs comes from provokingVertexLast alone - and leaving it off keeps VUID-VkGraphicsPipelineCreateInfo-topology-04884 disarmed, so a TRIANGLE_FAN pipeline may take LAST on any device. The blit pipeline routes through the same selector: it has no flat varying and no capture, but on a device without provokingVertexModePerPipeline a blit left on FIRST inside a render pass whose draws are LAST is an illegal mix. Per the POST rule the new extension gets rows for provokingVertexLast and for the two properties that change what MobileGL can promise. Fixes queries_functional on Magma (370/371). An A/B over a 976-case transform feedback / geometry shader / layered rendering subset of GL30-GL45 is otherwise identical on both backends and additionally takes 14 geometry_shader rendering and layered_rendering cases from failing to passing on Magma.
528 lines
27 KiB
C++
528 lines
27 KiB
C++
// 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.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.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);
|
|
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};
|
|
vpci.viewportCount = 1;
|
|
vpci.scissorCount = 1;
|
|
|
|
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;
|
|
|
|
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();
|
|
|
|
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
|
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
|
|
gpi.pStages = payload.stages->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);
|
|
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);
|
|
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
|