text: #include <chrono>
  
  #include "dxvk_device.h"
  #include "dxvk_graphics.h"
  #include "dxvk_pipemanager.h"
  #include "dxvk_spec_const.h"
  #include "dxvk_state_cache.h"
  
  namespace dxvk {
  
    DxvkGraphicsPipeline::DxvkGraphicsPipeline(
            DxvkPipelineManager*        pipeMgr,
            DxvkGraphicsPipelineShaders shaders)
    : m_vkd(pipeMgr->m_device->vkd()), m_pipeMgr(pipeMgr),
      m_shaders(std::move(shaders)) {
      if (m_shaders.vs  != nullptr) m_shaders.vs ->defineResourceSlots(m_slotMapping);
      if (m_shaders.tcs != nullptr) m_shaders.tcs->defineResourceSlots(m_slotMapping);
      if (m_shaders.tes != nullptr) m_shaders.tes->defineResourceSlots(m_slotMapping);
      if (m_shaders.gs  != nullptr) m_shaders.gs ->defineResourceSlots(m_slotMapping);
      if (m_shaders.fs  != nullptr) m_shaders.fs ->defineResourceSlots(m_slotMapping);
      
      m_slotMapping.makeDescriptorsDynamic(
        pipeMgr->m_device->options().maxNumDynamicUniformBuffers,
        pipeMgr->m_device->options().maxNumDynamicStorageBuffers);
      
      m_layout = new DxvkPipelineLayout(m_vkd,
        m_slotMapping, VK_PIPELINE_BIND_POINT_GRAPHICS);
      
      m_vsIn  = m_shaders.vs != nullptr ? m_shaders.vs->interfaceSlots().inputSlots  : 0;
      m_fsOut = m_shaders.fs != nullptr ? m_shaders.fs->interfaceSlots().outputSlots : 0;
  
      if (m_shaders.gs != nullptr && m_shaders.gs->hasCapability(spv::CapabilityTransformFeedback))
        m_flags.set(DxvkGraphicsPipelineFlag::HasTransformFeedback);
      
      if (m_layout->getStorageDescriptorStages())
        m_flags.set(DxvkGraphicsPipelineFlag::HasStorageDescriptors);
      
      m_common.msSampleShadingEnable = m_shaders.fs != nullptr && m_shaders.fs->hasCapability(spv::CapabilitySampleRateShading);
      m_common.msSampleShadingFactor = 1.0f;
    }
    
    
    DxvkGraphicsPipeline::~DxvkGraphicsPipeline() {
      for (const auto& instance : m_pipelines)
        this->destroyPipeline(instance.pipeline());
    }
    
    
    Rc<DxvkShader> DxvkGraphicsPipeline::getShader(
            VkShaderStageFlagBits             stage) const {
      switch (stage) {
        case VK_SHADER_STAGE_VERTEX_BIT:                  return m_shaders.vs;
        case VK_SHADER_STAGE_GEOMETRY_BIT:                return m_shaders.gs;
        case VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT:    return m_shaders.tcs;
        case VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT: return m_shaders.tes;
        case VK_SHADER_STAGE_FRAGMENT_BIT:                return m_shaders.fs;
        default:
          return nullptr;
      }
    }
  
  
    VkPipeline DxvkGraphicsPipeline::getPipelineHandle(
      const DxvkGraphicsPipelineStateInfo& state,
      const DxvkRenderPass*                renderPass) {
      DxvkGraphicsPipelineInstance* instance = nullptr;
  
      { std::lock_guard<sync::Spinlock> lock(m_mutex);
      
        instance = this->findInstance(state, renderPass);
        
        if (instance)
          return instance->pipeline();
        
        instance = this->createInstance(state, renderPass);
      }
      
      if (!instance)
        return VK_NULL_HANDLE;
  
      this->writePipelineStateToCache(state, renderPass->format());
      return instance->pipeline();
    }
  
  
    void DxvkGraphicsPipeline::compilePipeline(
      const DxvkGraphicsPipelineStateInfo& state,
      const DxvkRenderPass*                renderPass) {
      std::lock_guard<sync::Spinlock> lock(m_mutex);
  
      if (!this->findInstance(state, renderPass))
        this->createInstance(state, renderPass);
    }
  
  
    DxvkGraphicsPipelineInstance* DxvkGraphicsPipeline::createInstance(
      const DxvkGraphicsPipelineStateInfo& state,
      const DxvkRenderPass*                renderPass) {
      // If the pipeline state vector is invalid, don't try
      // to create a new pipeline, it won't work anyway.
      if (!this->validatePipelineState(state))
        return nullptr;
  
      VkPipeline newPipelineHandle = this->createPipeline(state, renderPass);
  
      m_pipeMgr->m_numGraphicsPipelines += 1;
      return &m_pipelines.emplace_back(state, renderPass, newPipelineHandle);
    }
    
    
    DxvkGraphicsPipelineInstance* DxvkGraphicsPipeline::findInstance(
      const DxvkGraphicsPipelineStateInfo& state,
      const DxvkRenderPass*                renderPass) {
      for (auto& instance : m_pipelines) {
        if (instance.isCompatible(state, renderPass))
          return &instance;
      }
      
      return nullptr;
    }
    
    
    VkPipeline DxvkGraphicsPipeline::createPipeline(
      const DxvkGraphicsPipelineStateInfo& state,
      const DxvkRenderPass*                renderPass) const {
      if (Logger::logLevel() <= LogLevel::Debug) {
        Logger::debug("Compiling graphics pipeline...");
        this->logPipelineState(LogLevel::Debug, state);
      }
  
      // Render pass format and image layouts
      DxvkRenderPassFormat passFormat = renderPass->format();
      
      // Set up dynamic states as needed
      std::array<VkDynamicState, 6> dynamicStates;
      uint32_t                      dynamicStateCount = 0;
      
      dynamicStates[dynamicStateCount++] = VK_DYNAMIC_STATE_VIEWPORT;
      dynamicStates[dynamicStateCount++] = VK_DYNAMIC_STATE_SCISSOR;
  
      if (state.useDynamicDepthBias())
        dynamicStates[dynamicStateCount++] = VK_DYNAMIC_STATE_DEPTH_BIAS;
      
      if (state.useDynamicDepthBounds())
        dynamicStates[dynamicStateCount++] = VK_DYNAMIC_STATE_DEPTH_BOUNDS;
      
      if (state.useDynamicBlendConstants())
        dynamicStates[dynamicStateCount++] = VK_DYNAMIC_STATE_BLEND_CONSTANTS;
      
      if (state.useDynamicStencilRef())
        dynamicStates[dynamicStateCount++] = VK_DYNAMIC_STATE_STENCIL_REFERENCE;
  
      // Figure out the actual sample count to use
      VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
  
      if (state.ms.sampleCount())
        sampleCount = VkSampleCountFlagBits(state.ms.sampleCount());
      else if (state.rs.sampleCount())
        sampleCount = VkSampleCountFlagBits(state.rs.sampleCount());
      
      // Set up some specialization constants
      DxvkSpecConstants specData;
      specData.set(uint32_t(DxvkSpecConstantId::RasterizerSampleCount), sampleCount, VK_SAMPLE_COUNT_1_BIT);
      
      for (uint32_t i = 0; i < m_layout->bindingCount(); i++)
        specData.set(i, state.bsBindingMask.test(i), true);
      
      for (uint32_t i = 0; i < MaxNumRenderTargets; i++) {
        if ((m_fsOut & (1 << i)) != 0) {
          uint32_t specId = uint32_t(DxvkSpecConstantId::ColorComponentMappings) + 4 * i;
          specData.set(specId + 0, state.omSwizzle[i].rIndex(), 0u);
          specData.set(specId + 1, state.omSwizzle[i].gIndex(), 1u);
          specData.set(specId + 2, state.omSwizzle[i].bIndex(), 2u);
          specData.set(specId + 3, state.omSwizzle[i].aIndex(), 3u);
        }
      }
  
      for (uint32_t i = 0; i < MaxNumSpecConstants; i++)
        specData.set(getSpecId(i), state.sc.specConstants[i], 0u);
      
      VkSpecializationInfo specInfo = specData.getSpecInfo();
      
      DxvkShaderModuleCreateInfo moduleInfo;
      moduleInfo.fsDualSrcBlend = state.omBlend[0].blendEnable() && (
        util::isDualSourceBlendFactor(state.omBlend[0].srcColorBlendFactor()) ||
        util::isDualSourceBlendFactor(state.omBlend[0].dstColorBlendFactor()) ||
        util::isDualSourceBlendFactor(state.omBlend[0].srcAlphaBlendFactor()) ||
        util::isDualSourceBlendFactor(state.omBlend[0].dstAlphaBlendFactor()));
      
      auto vsm  = createShaderModule(m_shaders.vs,  moduleInfo);
      auto gsm  = createShaderModule(m_shaders.gs,  moduleInfo);
      auto tcsm = createShaderModule(m_shaders.tcs, moduleInfo);
      auto tesm = createShaderModule(m_shaders.tes, moduleInfo);
      auto fsm  = createShaderModule(m_shaders.fs,  moduleInfo);
  
      std::vector<VkPipelineShaderStageCreateInfo> stages;
      if (vsm)  stages.push_back(vsm.stageInfo(&specInfo));
      if (tcsm) stages.push_back(tcsm.stageInfo(&specInfo));
      if (tesm) stages.push_back(tesm.stageInfo(&specInfo));
      if (gsm)  stages.push_back(gsm.stageInfo(&specInfo));
      if (fsm)  stages.push_back(fsm.stageInfo(&specInfo));
  
      // Fix up color write masks using the component mappings
      std::array<VkPipelineColorBlendAttachmentState, MaxNumRenderTargets> omBlendAttachments;
  
      const VkColorComponentFlags fullMask
        = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT
        | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
  
      for (uint32_t i = 0; i < MaxNumRenderTargets; i++) {
        omBlendAttachments[i] = state.omBlend[i].state();
  
        if (omBlendAttachments[i].colorWriteMask != fullMask) {
          omBlendAttachments[i].colorWriteMask = util::remapComponentMask(
            state.omBlend[i].colorWriteMask(), state.omSwizzle[i].mapping());
        }
        
        if ((m_fsOut & (1 << i)) == 0)
          omBlendAttachments[i].colorWriteMask = 0;
      }
  
      // Generate per-instance attribute divisors
      std::array<VkVertexInputBindingDivisorDescriptionEXT, MaxNumVertexBindings> viDivisorDesc;
      uint32_t                                                                    viDivisorCount = 0;
  
      for (uint32_t i = 0; i < state.il.bindingCount(); i++) {
        if (state.ilBindings[i].inputRate() == VK_VERTEX_INPUT_RATE_INSTANCE
         && state.ilBindings[i].divisor()   != 1) {
          const uint32_t id = viDivisorCount++;
          
          viDivisorDesc[id].binding = i; /* see below */
          viDivisorDesc[id].divisor = state.ilBindings[i].divisor();
        }
      }
  
      int32_t rasterizedStream = m_shaders.gs != nullptr
        ? m_shaders.gs->shaderOptions().rasterizedStream
        : 0;
      
      // Compact vertex bindings so that we can more easily update vertex buffers
      std::array<VkVertexInputAttributeDescription, MaxNumVertexAttributes> viAttribs;
      std::array<VkVertexInputBindingDescription,   MaxNumVertexBindings>   viBindings;
      std::array<uint32_t,                          MaxNumVertexBindings>   viBindingMap = { };
  
      for (uint32_t i = 0; i < state.il.bindingCount(); i++) {
        viBindings[i] = state.ilBindings[i].description();
        viBindings[i].binding = i;
        viBindingMap[state.ilBindings[i].binding()] = i;
      }
  
      for (uint32_t i = 0; i < state.il.attributeCount(); i++) {
        viAttribs[i] = state.ilAttributes[i].description();
        viAttribs[i].binding = viBindingMap[state.ilAttributes[i].binding()];
      }
  
      VkPipelineVertexInputDivisorStateCreateInfoEXT viDivisorInfo;
      viDivisorInfo.sType                     = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_DIVISOR_STATE_CREATE_INFO_EXT;
      viDivisorInfo.pNext                     = nullptr;
      viDivisorInfo.vertexBindingDivisorCount = viDivisorCount;
      viDivisorInfo.pVertexBindingDivisors    = viDivisorDesc.data();
      
      VkPipelineVertexInputStateCreateInfo viInfo;
      viInfo.sType                            = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
      viInfo.pNext                            = &viDivisorInfo;
      viInfo.flags                            = 0;
      viInfo.vertexBindingDescriptionCount    = state.il.bindingCount();
      viInfo.pVertexBindingDescriptions       = viBindings.data();
      viInfo.vertexAttributeDescriptionCount  = state.il.attributeCount();
      viInfo.pVertexAttributeDescriptions     = viAttribs.data();
      
      if (viDivisorCount == 0)
        viInfo.pNext = viDivisorInfo.pNext;
      
      // TODO remove this once the extension is widely supported
      if (!m_pipeMgr->m_device->features().extVertexAttributeDivisor.vertexAttributeInstanceRateDivisor)
        viInfo.pNext = viDivisorInfo.pNext;
      
      VkPipelineInputAssemblyStateCreateInfo iaInfo;
      iaInfo.sType                  = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
      iaInfo.pNext                  = nullptr;
      iaInfo.flags                  = 0;
      iaInfo.topology               = state.ia.primitiveTopology();
      iaInfo.primitiveRestartEnable = state.ia.primitiveRestart();
      
      VkPipelineTessellationStateCreateInfo tsInfo;
      tsInfo.sType                  = VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO;
      tsInfo.pNext                  = nullptr;
      tsInfo.flags                  = 0;
      tsInfo.patchControlPoints     = state.ia.patchVertexCount();
      
      VkPipelineViewportStateCreateInfo vpInfo;
      vpInfo.sType                  = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
      vpInfo.pNext                  = nullptr;
      vpInfo.flags                  = 0;
      vpInfo.viewportCount          = state.rs.viewportCount();
      vpInfo.pViewports             = nullptr;
      vpInfo.scissorCount           = state.rs.viewportCount();
      vpInfo.pScissors              = nullptr;
      
      VkPipelineRasterizationStateStreamCreateInfoEXT xfbStreamInfo;
      xfbStreamInfo.sType           = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_STREAM_CREATE_INFO_EXT;
      xfbStreamInfo.pNext           = nullptr;
      xfbStreamInfo.flags           = 0;
      xfbStreamInfo.rasterizationStream = uint32_t(rasterizedStream);
  
      VkPipelineRasterizationDepthClipStateCreateInfoEXT rsDepthClipInfo;
      rsDepthClipInfo.sType         = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_DEPTH_CLIP_STATE_CREATE_INFO_EXT;
      rsDepthClipInfo.pNext         = nullptr;
      rsDepthClipInfo.flags         = 0;
      rsDepthClipInfo.depthClipEnable = state.rs.depthClipEnable();
  
      VkPipelineRasterizationStateCreateInfo rsInfo;
      rsInfo.sType                  = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
      rsInfo.pNext                  = &rsDepthClipInfo;
      rsInfo.flags                  = 0;
      rsInfo.depthClampEnable       = VK_TRUE;
      rsInfo.rasterizerDiscardEnable = rasterizedStream < 0;
      rsInfo.polygonMode            = state.rs.polygonMode();
      rsInfo.cullMode               = state.rs.cullMode();
      rsInfo.frontFace              = state.rs.frontFace();
      rsInfo.depthBiasEnable        = state.rs.depthBiasEnable();
      rsInfo.depthBiasConstantFactor= 0.0f;
      rsInfo.depthBiasClamp         = 0.0f;
      rsInfo.depthBiasSlopeFactor   = 0.0f;
      rsInfo.lineWidth              = 1.0f;
      
      if (rasterizedStream > 0)
        rsDepthClipInfo.pNext = &xfbStreamInfo;
      
      if (!m_pipeMgr->m_device->features().extDepthClipEnable.depthClipEnable) {
        rsInfo.pNext                = rsDepthClipInfo.pNext;
        rsInfo.depthClampEnable     = !state.rs.depthClipEnable();
      }
  
      uint32_t sampleMask = state.ms.sampleMask();
  
      VkPipelineMultisampleStateCreateInfo msInfo;
      msInfo.sType                  = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
      msInfo.pNext                  = nullptr;
      msInfo.flags                  = 0;
      msInfo.rasterizationSamples   = sampleCount;
      msInfo.sampleShadingEnable    = m_common.msSampleShadingEnable;
      msInfo.minSampleShading       = m_common.msSampleShadingFactor;
      msInfo.pSampleMask            = &sampleMask;
      msInfo.alphaToCoverageEnable  = state.ms.enableAlphaToCoverage();
      msInfo.alphaToOneEnable       = VK_FALSE;
      
      VkPipelineDepthStencilStateCreateInfo dsInfo;
      dsInfo.sType                  = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
      dsInfo.pNext                  = nullptr;
      dsInfo.flags                  = 0;
      dsInfo.depthTestEnable        = state.ds.enableDepthTest();
      dsInfo.depthWriteEnable       = state.ds.enableDepthWrite() && !util::isDepthReadOnlyLayout(passFormat.depth.layout);
      dsInfo.depthCompareOp         = state.ds.depthCompareOp();
      dsInfo.depthBoundsTestEnable  = state.ds.enableDepthBoundsTest();
      dsInfo.stencilTestEnable      = state.ds.enableStencilTest();
      dsInfo.front                  = state.dsFront.state();
      dsInfo.back                   = state.dsBack.state();
      dsInfo.minDepthBounds         = 0.0f;
      dsInfo.maxDepthBounds         = 1.0f;
      
      VkPipelineColorBlendStateCreateInfo cbInfo;
      cbInfo.sType                  = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
      cbInfo.pNext                  = nullptr;
      cbInfo.flags                  = 0;
      cbInfo.logicOpEnable          = state.om.enableLogicOp();
      cbInfo.logicOp                = state.om.logicOp();
      cbInfo.attachmentCount        = DxvkLimits::MaxNumRenderTargets;
      cbInfo.pAttachments           = omBlendAttachments.data();
      
      for (uint32_t i = 0; i < 4; i++)
        cbInfo.blendConstants[i] = 0.0f;
      
      VkPipelineDynamicStateCreateInfo dyInfo;
      dyInfo.sType                  = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
      dyInfo.pNext                  = nullptr;
      dyInfo.flags                  = 0;
      dyInfo.dynamicStateCount      = dynamicStateCount;
      dyInfo.pDynamicStates         = dynamicStates.data();
      
      VkGraphicsPipelineCreateInfo info;
      info.sType                    = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
      info.pNext                    = nullptr;
      info.flags                    = 0;
      info.stageCount               = stages.size();
      info.pStages                  = stages.data();
      info.pVertexInputState        = &viInfo;
      info.pInputAssemblyState      = &iaInfo;
      info.pTessellationState       = &tsInfo;
      info.pViewportState           = &vpInfo;
      info.pRasterizationState      = &rsInfo;
      info.pMultisampleState        = &msInfo;
      info.pDepthStencilState       = &dsInfo;
      info.pColorBlendState         = &cbInfo;
      info.pDynamicState            = &dyInfo;
      info.layout                   = m_layout->pipelineLayout();
      info.renderPass               = renderPass->getDefaultHandle();
      info.subpass                  = 0;
      info.basePipelineHandle       = VK_NULL_HANDLE;
      info.basePipelineIndex        = -1;
      
      if (tsInfo.patchControlPoints == 0)
        info.pTessellationState = nullptr;
      
      // Time pipeline compilation for debugging purposes
      std::chrono::high_resolution_clock::time_point t0, t1;
  
      if (Logger::logLevel() <= LogLevel::Debug)
        t0 = std::chrono::high_resolution_clock::now();
      
      VkPipeline pipeline = VK_NULL_HANDLE;
      if (m_vkd->vkCreateGraphicsPipelines(m_vkd->device(),
            m_pipeMgr->m_cache->handle(), 1, &info, nullptr, &pipeline) != VK_SUCCESS) {
        Logger::err("DxvkGraphicsPipeline: Failed to compile pipeline");
        this->logPipelineState(LogLevel::Error, state);
        return VK_NULL_HANDLE;
      }
      
      if (Logger::logLevel() <= LogLevel::Debug) {
        t1 = std::chrono::high_resolution_clock::now();
        auto td = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0);
        Logger::debug(str::format("DxvkGraphicsPipeline: Finished in ", td.count(), " ms"));
      }
  
      return pipeline;
    }
    
    
    void DxvkGraphicsPipeline::destroyPipeline(VkPipeline pipeline) const {
      m_vkd->vkDestroyPipeline(m_vkd->device(), pipeline, nullptr);
    }
  
  
    DxvkShaderModule DxvkGraphicsPipeline::createShaderModule(
      const Rc<DxvkShader>&                shader,
      const DxvkShaderModuleCreateInfo&    info) const {
      return shader != nullptr
        ? shader->createShaderModule(m_vkd, m_slotMapping, info)
        : DxvkShaderModule();
    }
  
  
    bool DxvkGraphicsPipeline::validatePipelineState(
      const DxvkGraphicsPipelineStateInfo& state) const {
      // Validate vertex input - each input slot consumed by the
      // vertex shader must be provided by the input layout.
      uint32_t providedVertexInputs = 0;
      
      for (uint32_t i = 0; i < state.il.attributeCount(); i++)
        providedVertexInputs |= 1u << state.ilAttributes[i].location();
      
      if ((providedVertexInputs & m_vsIn) != m_vsIn)
        return false;
      
      // Tessellation shaders and patches must be used together
      bool hasPatches = state.ia.primitiveTopology() == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
  
      bool hasTcs = m_shaders.tcs != nullptr;
      bool hasTes = m_shaders.tes != nullptr;
  
      if (hasPatches != hasTcs || hasPatches != hasTes)
        return false;
      
      // Filter out undefined primitive topologies
      if (state.ia.primitiveTopology() == VK_PRIMITIVE_TOPOLOGY_MAX_ENUM)
        return false;
      
      // Prevent unintended out-of-bounds access to the IL arrays
      if (state.il.attributeCount() > DxvkLimits::MaxNumVertexAttributes
       || state.il.bindingCount()   > DxvkLimits::MaxNumVertexBindings)
        return false;
      
      // No errors
      return true;
    }
    
    
    void DxvkGraphicsPipeline::writePipelineStateToCache(
      const DxvkGraphicsPipelineStateInfo& state,
      const DxvkRenderPassFormat&          format) const {
      if (m_pipeMgr->m_stateCache == nullptr)
        return;
      
      DxvkStateCacheKey key;
      if (m_shaders.vs  != nullptr) key.vs = m_shaders.vs->getShaderKey();
      if (m_shaders.tcs != nullptr) key.tcs = m_shaders.tcs->getShaderKey();
      if (m_shaders.tes != nullptr) key.tes = m_shaders.tes->getShaderKey();
      if (m_shaders.gs  != nullptr) key.gs = m_shaders.gs->getShaderKey();
      if (m_shaders.fs  != nullptr) key.fs = m_shaders.fs->getShaderKey();
  
      m_pipeMgr->m_stateCache->addGraphicsPipeline(key, state, format);
    }
    
    
    void DxvkGraphicsPipeline::logPipelineState(
            LogLevel                       level,
      const DxvkGraphicsPipelineStateInfo& state) const {
      if (m_shaders.vs  != nullptr) Logger::log(level, str::format("  vs  : ", m_shaders.vs ->debugName()));
      if (m_shaders.tcs != nullptr) Logger::log(level, str::format("  tcs : ", m_shaders.tcs->debugName()));
      if (m_shaders.tes != nullptr) Logger::log(level, str::format("  tes : ", m_shaders.tes->debugName()));
      if (m_shaders.gs  != nullptr) Logger::log(level, str::format("  gs  : ", m_shaders.gs ->debugName()));
      if (m_shaders.fs  != nullptr) Logger::log(level, str::format("  fs  : ", m_shaders.fs ->debugName()));
  
      for (uint32_t i = 0; i < state.il.attributeCount(); i++) {
        const auto& attr = state.ilAttributes[i];
        Logger::log(level, str::format("  attr ", i, " : location ", attr.location(), ", binding ", attr.binding(), ", format ", attr.format(), ", offset ", attr.offset()));
      }
      for (uint32_t i = 0; i < state.il.bindingCount(); i++) {
        const auto& bind = state.ilBindings[i];
        Logger::log(level, str::format("  binding ", i, " : binding ", bind.binding(), ", stride ", bind.stride(), ", rate ", bind.inputRate(), ", divisor ", bind.divisor()));
      }
      
      // TODO log more pipeline state
    }
    
  }
  