#include "VulkanPipeline.h" #include "VulkanConvert.h" #include "cfile/cfile.h" #include namespace graphics::vulkan { // Global pipeline manager pointer static VulkanPipelineManager* g_pipelineManager = nullptr; VulkanPipelineManager* getPipelineManager() { Assertion(g_pipelineManager != nullptr, "Vulkan PipelineManager not initialized!"); return g_pipelineManager; } void setPipelineManager(VulkanPipelineManager* manager) { g_pipelineManager = manager; } bool PipelineConfig::operator==(const PipelineConfig& other) const { return shaderType == other.shaderType && shaderFlags == other.shaderFlags && vertexLayoutHash == other.vertexLayoutHash && primitiveType == other.primitiveType && depthMode == other.depthMode && blendMode == other.blendMode && cullEnabled == other.cullEnabled && frontFaceCW == other.frontFaceCW && depthWriteEnabled == other.depthWriteEnabled && stencilEnabled == other.stencilEnabled && stencilFunc == other.stencilFunc && stencilMask == other.stencilMask && frontStencilOp.stencilFailOperation == other.frontStencilOp.stencilFailOperation && frontStencilOp.depthFailOperation == other.frontStencilOp.depthFailOperation && frontStencilOp.successOperation == other.frontStencilOp.successOperation && backStencilOp.stencilFailOperation == other.backStencilOp.stencilFailOperation && backStencilOp.depthFailOperation == other.backStencilOp.depthFailOperation && backStencilOp.successOperation == other.backStencilOp.successOperation && colorWriteMask.x == other.colorWriteMask.x && colorWriteMask.y == other.colorWriteMask.y && colorWriteMask.z == other.colorWriteMask.z && colorWriteMask.w == other.colorWriteMask.w && fillMode == other.fillMode && depthBiasEnabled == other.depthBiasEnabled && depthClampEnabled == other.depthClampEnabled && renderPass == other.renderPass && subpass == other.subpass && colorAttachmentCount == other.colorAttachmentCount && sampleCount == other.sampleCount && perAttachmentBlendEnabled == other.perAttachmentBlendEnabled && [&]() { if (!perAttachmentBlendEnabled) return true; for (uint32_t i = 0; i < colorAttachmentCount; ++i) { if (attachmentBlends[i].blendMode != other.attachmentBlends[i].blendMode || attachmentBlends[i].writeMask.x != other.attachmentBlends[i].writeMask.x || attachmentBlends[i].writeMask.y != other.attachmentBlends[i].writeMask.y || attachmentBlends[i].writeMask.z != other.attachmentBlends[i].writeMask.z || attachmentBlends[i].writeMask.w != other.attachmentBlends[i].writeMask.w) return false; } return true; }(); } namespace { // boost::hash_combine-style 64-bit fold (same pattern as vertex_layout::hash()). // Order-dependent and avalanches every field into all bits, unlike the previous // `h ^= hash(field) << shift` scheme whose overlapping/wrapping shifts collided // heavily. operator== remains the authority for equality; a better hash only // reduces bucket collisions (fewer operator== calls per lookup). inline void hashCombine(uint64_t& seed, uint64_t value) { seed ^= value + 0x9e3779b97f4a7c15ull + (seed << 6) + (seed >> 2); } inline uint64_t maskBits(bool x, bool y, bool z, bool w) { return (x ? 1u : 0u) | (y ? 2u : 0u) | (z ? 4u : 0u) | (w ? 8u : 0u); } // Non-dispatchable Vulkan handles (e.g. VkRenderPass) are pointers on 64-bit // platforms but plain uint64_t on 32-bit ones (VK_USE_64_BIT_PTR_DEFINES), so // the pointer/integer branch can't be collapsed into a single reinterpret_cast. template inline uint64_t handleToU64(T handle) { if constexpr (std::is_pointer_v) { return reinterpret_cast(handle); } else { return static_cast(handle); } } } // namespace uint64_t PipelineConfig::hash() const { uint64_t h = 0; // Fold every field operator== compares, so equal configs always hash equal. hashCombine(h, static_cast(shaderType)); hashCombine(h, shaderFlags); hashCombine(h, vertexLayoutHash); hashCombine(h, static_cast(primitiveType)); hashCombine(h, static_cast(depthMode)); hashCombine(h, static_cast(blendMode)); hashCombine(h, cullEnabled ? 1u : 0u); hashCombine(h, frontFaceCW ? 1u : 0u); hashCombine(h, depthWriteEnabled ? 1u : 0u); hashCombine(h, stencilEnabled ? 1u : 0u); hashCombine(h, static_cast(stencilFunc)); hashCombine(h, stencilMask); hashCombine(h, static_cast(frontStencilOp.stencilFailOperation)); hashCombine(h, static_cast(frontStencilOp.depthFailOperation)); hashCombine(h, static_cast(frontStencilOp.successOperation)); hashCombine(h, static_cast(backStencilOp.stencilFailOperation)); hashCombine(h, static_cast(backStencilOp.depthFailOperation)); hashCombine(h, static_cast(backStencilOp.successOperation)); hashCombine(h, maskBits(colorWriteMask.x, colorWriteMask.y, colorWriteMask.z, colorWriteMask.w)); hashCombine(h, static_cast(fillMode)); hashCombine(h, depthBiasEnabled ? 1u : 0u); hashCombine(h, depthClampEnabled ? 1u : 0u); hashCombine(h, handleToU64(static_cast(renderPass))); hashCombine(h, subpass); hashCombine(h, colorAttachmentCount); hashCombine(h, static_cast(sampleCount)); hashCombine(h, perAttachmentBlendEnabled ? 1u : 0u); if (perAttachmentBlendEnabled) { for (uint32_t i = 0; i < colorAttachmentCount; ++i) { hashCombine(h, static_cast(attachmentBlends[i].blendMode)); hashCombine(h, maskBits(attachmentBlends[i].writeMask.x, attachmentBlends[i].writeMask.y, attachmentBlends[i].writeMask.z, attachmentBlends[i].writeMask.w)); } } return h; } bool VulkanPipelineManager::init(vk::Device device, VulkanShaderManager* shaderManager, VulkanDescriptorManager* descriptorManager) { if (m_initialized) { return true; } m_device = device; m_shaderManager = shaderManager; m_descriptorManager = descriptorManager; // Create empty pipeline cache vk::PipelineCacheCreateInfo cacheInfo; m_pipelineCache = m_device.createPipelineCacheUnique(cacheInfo); // Create common pipeline layout createPipelineLayout(); m_initialized = true; nprintf(("vulkan", "VulkanPipelineManager: Initialized\n")); return true; } void VulkanPipelineManager::shutdown() { if (!m_initialized) { return; } // Wait for device idle m_device.waitIdle(); // Clear all pipelines m_pipelines.clear(); m_pipelineLayout.reset(); m_pipelineCache.reset(); m_vertexFormatCache.clear(); m_initialized = false; nprintf(("vulkan", "VulkanPipelineManager: Shutdown complete\n")); } vk::Pipeline VulkanPipelineManager::getPipeline(const PipelineConfig& config, const vertex_layout& vertexLayout) { Assertion(m_initialized, "VulkanPipelineManager::getPipeline called before initialization!"); // Update vertex layout hash in config PipelineConfig fullConfig = config; fullConfig.vertexLayoutHash = vertexLayout.hash(); // Check cache auto it = m_pipelines.find(fullConfig); if (it != m_pipelines.end()) { return it->second.get(); } // Create new pipeline auto pipeline = createPipeline(fullConfig, vertexLayout); if (!pipeline) { return {}; } vk::Pipeline result = pipeline.get(); m_pipelines[fullConfig] = std::move(pipeline); nprintf(("Vulkan", "VulkanPipelineManager: Created pipeline for shader type %d (hash 0x%" PRIx64 ")\n", static_cast(config.shaderType), fullConfig.hash())); return result; } bool VulkanPipelineManager::loadPipelineCache(const SCP_string& filename) { // Try to load cache file CFILE* fp = cfopen(filename.c_str(), "rb", CF_TYPE_CACHE); if (!fp) { nprintf(("Vulkan", "VulkanPipelineManager: No pipeline cache file found: %s\n", filename.c_str())); return false; } // Get file size int fileSize = cfilelength(fp); if (fileSize <= 0) { cfclose(fp); return false; } // Read cache data SCP_vector cacheData(fileSize); if (cfread(cacheData.data(), 1, fileSize, fp) != fileSize) { cfclose(fp); return false; } cfclose(fp); // Create new pipeline cache with data vk::PipelineCacheCreateInfo cacheInfo; cacheInfo.initialDataSize = cacheData.size(); cacheInfo.pInitialData = cacheData.data(); try { auto newCache = m_device.createPipelineCacheUnique(cacheInfo); m_pipelineCache = std::move(newCache); nprintf(("vulkan", "VulkanPipelineManager: Loaded pipeline cache: %s (%d bytes)\n", filename.c_str(), fileSize)); return true; } catch (const vk::SystemError& e) { nprintf(("vulkan", "VulkanPipelineManager: Failed to load pipeline cache: %s\n", e.what())); return false; } } bool VulkanPipelineManager::savePipelineCache(const SCP_string& filename) { if (!m_pipelineCache) { return false; } // Get cache data auto cacheData = m_device.getPipelineCacheData(m_pipelineCache.get()); if (cacheData.empty()) { return false; } // Write to file CFILE* fp = cfopen(filename.c_str(), "wb", CF_TYPE_CACHE); if (!fp) { nprintf(("vulkan", "VulkanPipelineManager: Could not create cache file: %s\n", filename.c_str())); return false; } bool success = (cfwrite(cacheData.data(), 1, static_cast(cacheData.size()), fp) == static_cast(cacheData.size())); cfclose(fp); if (success) { nprintf(("vulkan", "VulkanPipelineManager: Saved pipeline cache: %s (%zu bytes)\n", filename.c_str(), cacheData.size())); } return success; } bool VulkanPipelineManager::needsFallbackAttribute(const vertex_layout& vertexLayout, shader_type shaderType, VertexAttributeLocation location) { // Empty layouts (fullscreen triangle etc.) don't use fallbacks if (vertexLayout.get_num_vertex_components() == 0) return false; const VertexInputConfig& config = m_vertexFormatCache.getVertexInputConfig(vertexLayout); uint32_t bit = 1u << location; // Layout natively provides this attribute — no fallback needed if (config.providedInputMask & bit) return false; // Fallback needed only if the shader actually consumes this attribute const VulkanShaderModule* shader = m_shaderManager->getShaderByType(shaderType); if (shader && shader->vertexInputMask != 0) { return (shader->vertexInputMask & bit) != 0; } return true; } void VulkanPipelineManager::createPipelineLayout() { // Get descriptor set layouts from descriptor manager const auto& uniqueLayouts = m_descriptorManager->getAllSetLayouts(); std::array(DescriptorSetIndex::Count)> setLayouts; for (size_t i = 0; i < uniqueLayouts.size(); ++i) { setLayouts[i] = uniqueLayouts[i].get(); } vk::PipelineLayoutCreateInfo layoutInfo; layoutInfo.setLayoutCount = static_cast(setLayouts.size()); layoutInfo.pSetLayouts = setLayouts.data(); layoutInfo.pushConstantRangeCount = 0; layoutInfo.pPushConstantRanges = nullptr; m_pipelineLayout = m_device.createPipelineLayoutUnique(layoutInfo); nprintf(("vulkan", "VulkanPipelineManager: Created pipeline layout with %zu descriptor sets\n", setLayouts.size())); } vk::UniquePipeline VulkanPipelineManager::createPipeline(const PipelineConfig& config, const vertex_layout& vertexLayout) { // Ensure shader variant is loaded (lazy creation on first use) int shaderHandle = m_shaderManager->maybeCreateShader(config.shaderType, config.shaderFlags); // Get shader modules const VulkanShaderModule* shader = (shaderHandle >= 0) ? m_shaderManager->getShader(shaderHandle) : nullptr; if (!shader || !shader->valid) { nprintf(("vulkan", "VulkanPipelineManager: Shader not available for type %d\n", static_cast(config.shaderType))); return {}; } // Debug: Log which shader and vertex layout is being used nprintf(("vulkan", "VulkanPipelineManager: Creating pipeline for shader type %d (%s)\n", static_cast(config.shaderType), shader->description.c_str())); nprintf(("vulkan", " Vertex layout has %zu components:\n", vertexLayout.get_num_vertex_components())); for (size_t i = 0; i < vertexLayout.get_num_vertex_components(); ++i) { const vertex_format_data* comp = vertexLayout.get_vertex_component(i); nprintf(("vulkan", " [%zu] format=%d offset=%zu stride=%zu\n", i, static_cast(comp->format_type), comp->offset, comp->stride)); } // Shader stages SCP_vector shaderStages; vk::PipelineShaderStageCreateInfo vertStage; vertStage.stage = vk::ShaderStageFlagBits::eVertex; vertStage.module = shader->vertexModule.get(); vertStage.pName = "main"; shaderStages.push_back(vertStage); vk::PipelineShaderStageCreateInfo fragStage; fragStage.stage = vk::ShaderStageFlagBits::eFragment; fragStage.module = shader->fragmentModule.get(); fragStage.pName = "main"; shaderStages.push_back(fragStage); // Vertex input state — filter out attributes the shader doesn't consume. // The vertex format cache may add fallback color/texcoord attributes that // shaders like NanoVG don't declare; the SPIR-V compiler strips unused // inputs, so we must match the pipeline to the actual shader inputs. VertexInputConfig vertexInputConfig = m_vertexFormatCache.getVertexInputConfig(vertexLayout); if (shader->vertexInputMask != 0) { uint32_t mask = shader->vertexInputMask; auto& attrs = vertexInputConfig.attributes; SCP_unordered_set usedBindings; // Remove attributes at locations the shader doesn't use attrs.erase(std::remove_if(attrs.begin(), attrs.end(), [mask](const vk::VertexInputAttributeDescription& a) { return (mask & (1u << a.location)) == 0; }), attrs.end()); // Collect bindings still referenced by remaining attributes for (auto& a : attrs) { usedBindings.insert(a.binding); } // Remove orphaned bindings auto& binds = vertexInputConfig.bindings; binds.erase(std::remove_if(binds.begin(), binds.end(), [&usedBindings](const vk::VertexInputBindingDescription& b) { return usedBindings.count(b.binding) == 0; }), binds.end()); vertexInputConfig.updatePointers(); } // Input assembly // // Primitive restart is enabled for every topology that permits it, rather than disabled // outright. Metal always performs primitive restart on indexed draws and cannot turn it off, so // MoltenVK rejects a strip/fan pipeline that asks to disable it -- vkCreateGraphicsPipelines // fails with VK_ERROR_FEATURE_NOT_PRESENT ("Metal does not support disabling primitive // restart"). getPipeline() then returns a null pipeline and the draw is skipped, which on macOS // silently dropped the entire 2D/UI/HUD path (gr_bitmap and friends render quads as // PRIM_TYPE_TRISTRIP; see graphics/render.cpp) while leaving ImGui -- which builds its own // TRIANGLE_LIST pipeline -- as the only thing still on screen. // // Enabling it changes nothing for FSO's geometry: an index buffer is promoted to 32-bit as soon // as a mesh reaches USHRT_MAX vertices (VB_FLAG_LARGE_INDEX, see modelinterp.cpp), so 0xFFFF is // never a real 16-bit index, and 0xFFFFFFFF is unreachable for 32-bit ones. Neither restart // sentinel can collide with live index data. // // List topologies must keep it disabled: the spec only allows primitiveRestartEnable on them // with VK_EXT_primitive_topology_list_restart, which FSO does not request. vk::PipelineInputAssemblyStateCreateInfo inputAssembly; inputAssembly.topology = convertPrimitiveType(config.primitiveType); inputAssembly.primitiveRestartEnable = topologySupportsPrimitiveRestart(inputAssembly.topology); // Viewport state (dynamic) vk::PipelineViewportStateCreateInfo viewportState; viewportState.viewportCount = 1; viewportState.pViewports = nullptr; // Dynamic viewportState.scissorCount = 1; viewportState.pScissors = nullptr; // Dynamic // Rasterization state vk::PipelineRasterizationStateCreateInfo rasterizer = createRasterizationState( config.cullEnabled, config.fillMode, config.frontFaceCW, config.depthBiasEnabled, config.depthClampEnabled); // Multisample state vk::PipelineMultisampleStateCreateInfo multisampling; multisampling.rasterizationSamples = config.sampleCount; multisampling.sampleShadingEnable = VK_FALSE; // Depth stencil state vk::PipelineDepthStencilStateCreateInfo depthStencil = createDepthStencilState( config.depthMode, config.stencilEnabled, config.stencilFunc, config.stencilEnabled ? &config.frontStencilOp : nullptr, config.stencilEnabled ? &config.backStencilOp : nullptr, config.stencilMask); // Override depth write if specified if (!config.depthWriteEnabled) { depthStencil.depthWriteEnable = VK_FALSE; } // Color blend state SCP_vector colorBlendAttachments; for (uint32_t i = 0; i < config.colorAttachmentCount; ++i) { if (config.perAttachmentBlendEnabled) { colorBlendAttachments.push_back(createColorBlendAttachment( config.attachmentBlends[i].blendMode, config.attachmentBlends[i].writeMask)); } else { colorBlendAttachments.push_back(createColorBlendAttachment(config.blendMode, config.colorWriteMask)); } } vk::PipelineColorBlendStateCreateInfo colorBlending; colorBlending.logicOpEnable = VK_FALSE; colorBlending.attachmentCount = static_cast(colorBlendAttachments.size()); colorBlending.pAttachments = colorBlendAttachments.data(); // Dynamic state std::array dynamicStates = { vk::DynamicState::eViewport, vk::DynamicState::eScissor, vk::DynamicState::eLineWidth, vk::DynamicState::eDepthBias, vk::DynamicState::eStencilReference, }; vk::PipelineDynamicStateCreateInfo dynamicState; dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); dynamicState.pDynamicStates = dynamicStates.data(); // Create pipeline vk::GraphicsPipelineCreateInfo pipelineInfo; pipelineInfo.stageCount = static_cast(shaderStages.size()); pipelineInfo.pStages = shaderStages.data(); pipelineInfo.pVertexInputState = &vertexInputConfig.createInfo; pipelineInfo.pInputAssemblyState = &inputAssembly; pipelineInfo.pViewportState = &viewportState; pipelineInfo.pRasterizationState = &rasterizer; pipelineInfo.pMultisampleState = &multisampling; pipelineInfo.pDepthStencilState = &depthStencil; pipelineInfo.pColorBlendState = &colorBlending; pipelineInfo.pDynamicState = &dynamicState; pipelineInfo.layout = m_pipelineLayout.get(); pipelineInfo.renderPass = config.renderPass; pipelineInfo.subpass = config.subpass; pipelineInfo.basePipelineHandle = nullptr; pipelineInfo.basePipelineIndex = -1; try { auto result = m_device.createGraphicsPipelineUnique(m_pipelineCache.get(), pipelineInfo); return std::move(result.value); } catch (const vk::SystemError& e) { nprintf(("vulkan", "VulkanPipelineManager: Failed to create pipeline: %s\n", e.what())); return {}; } } } // namespace graphics::vulkan