From aac66a1b61a0e4c1c2b85d89bc818a8b521bcffa Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sun, 13 Nov 2016 18:39:06 +1000 Subject: [PATCH] Vulkan: Implement a pipeline UID cache This stores enough information to recreate the pipeline, including the shader UIDs, blend/depth/rasterization state, primitive and vertex format. --- .../Core/VideoBackends/Vulkan/ObjectCache.cpp | 28 +++- .../Core/VideoBackends/Vulkan/ObjectCache.h | 16 +- Source/Core/VideoBackends/Vulkan/Renderer.cpp | 4 + .../VideoBackends/Vulkan/StateTracker.cpp | 145 +++++++++++++++--- .../Core/VideoBackends/Vulkan/StateTracker.h | 56 ++++++- .../VideoBackends/Vulkan/VertexFormat.cpp | 13 ++ .../Core/VideoBackends/Vulkan/VertexFormat.h | 5 + 7 files changed, 227 insertions(+), 40 deletions(-) diff --git a/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp b/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp index 61e685874a..ecfc73c76e 100644 --- a/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp +++ b/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp @@ -159,12 +159,8 @@ GetVulkanColorBlendState(const BlendState& state, return vk_state; } -VkPipeline ObjectCache::GetPipeline(const PipelineInfo& info) +VkPipeline ObjectCache::CreatePipeline(const PipelineInfo& info) { - auto iter = m_pipeline_objects.find(info); - if (iter != m_pipeline_objects.end()) - return iter->second; - // Declare descriptors for empty vertex buffers/attributes static const VkPipelineVertexInputStateCreateInfo empty_vertex_input_state = { VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, // VkStructureType sType @@ -278,16 +274,34 @@ VkPipeline ObjectCache::GetPipeline(const PipelineInfo& info) -1 // int32_t basePipelineIndex }; - VkPipeline pipeline = VK_NULL_HANDLE; + VkPipeline pipeline; VkResult res = vkCreateGraphicsPipelines(g_vulkan_context->GetDevice(), m_pipeline_cache, 1, &pipeline_info, nullptr, &pipeline); if (res != VK_SUCCESS) + { LOG_VULKAN_ERROR(res, "vkCreateGraphicsPipelines failed: "); + return VK_NULL_HANDLE; + } - m_pipeline_objects.emplace(info, pipeline); return pipeline; } +VkPipeline ObjectCache::GetPipeline(const PipelineInfo& info) +{ + return GetPipelineWithCacheResult(info).first; +} + +std::pair ObjectCache::GetPipelineWithCacheResult(const PipelineInfo& info) +{ + auto iter = m_pipeline_objects.find(info); + if (iter != m_pipeline_objects.end()) + return {iter->second, true}; + + VkPipeline pipeline = CreatePipeline(info); + m_pipeline_objects.emplace(info, pipeline); + return {pipeline, false}; +} + std::string ObjectCache::GetDiskCacheFileName(const char* type) { return StringFromFormat("%svulkan-%s-%s.cache", File::GetUserPath(D_SHADERCACHE_IDX).c_str(), diff --git a/Source/Core/VideoBackends/Vulkan/ObjectCache.h b/Source/Core/VideoBackends/Vulkan/ObjectCache.h index b991b8a3b8..e102440164 100644 --- a/Source/Core/VideoBackends/Vulkan/ObjectCache.h +++ b/Source/Core/VideoBackends/Vulkan/ObjectCache.h @@ -111,9 +111,18 @@ public: // Perform at startup, create descriptor layouts, compiles all static shaders. bool Initialize(); - // Find a pipeline by the specified description, if not found, attempts to create it + // Creates a pipeline for the specified description. The resulting pipeline, if successful + // is not stored anywhere, this is left up to the caller. + VkPipeline CreatePipeline(const PipelineInfo& info); + + // Find a pipeline by the specified description, if not found, attempts to create it. VkPipeline GetPipeline(const PipelineInfo& info); + // Find a pipeline by the specified description, if not found, attempts to create it. If this + // resulted in a pipeline being created, the second field of the return value will be false, + // otherwise for a cache hit it will be true. + std::pair GetPipelineWithCacheResult(const PipelineInfo& info); + // Wipes out the pipeline cache, use when MSAA modes change, for example // Also destroys the data that would be stored in the disk cache. void ClearPipelineCache(); @@ -133,6 +142,9 @@ public: VkShaderModule GetPassthroughVertexShader() const { return m_passthrough_vertex_shader; } VkShaderModule GetScreenQuadGeometryShader() const { return m_screen_quad_geometry_shader; } VkShaderModule GetPassthroughGeometryShader() const { return m_passthrough_geometry_shader; } + // Gets the filename of the specified type of cache object (e.g. vertex shader, pipeline). + std::string GetDiskCacheFileName(const char* type); + private: bool CreatePipelineCache(bool load_from_disk); void DestroyPipelineCache(); @@ -148,8 +160,6 @@ private: void DestroySharedShaders(); void DestroySamplers(); - std::string GetDiskCacheFileName(const char* type); - std::array m_descriptor_set_layouts = {}; VkPipelineLayout m_standard_pipeline_layout = VK_NULL_HANDLE; diff --git a/Source/Core/VideoBackends/Vulkan/Renderer.cpp b/Source/Core/VideoBackends/Vulkan/Renderer.cpp index 5348d6beec..05ae219ec2 100644 --- a/Source/Core/VideoBackends/Vulkan/Renderer.cpp +++ b/Source/Core/VideoBackends/Vulkan/Renderer.cpp @@ -116,6 +116,9 @@ bool Renderer::Initialize() m_bounding_box->GetGPUBufferSize()); } + // Ensure all pipelines previously used by the game have been created. + StateTracker::GetInstance()->LoadPipelineUIDCache(); + // Various initialization routines will have executed commands on the command buffer. // Execute what we have done before beginning the first frame. g_command_buffer_mgr->PrepareToSubmitCommandBuffer(); @@ -1136,6 +1139,7 @@ void Renderer::CheckForConfigChanges() FramebufferManager::GetInstance()->RecompileShaders(); g_object_cache->ClearPipelineCache(); g_object_cache->RecompileSharedShaders(); + StateTracker::GetInstance()->LoadPipelineUIDCache(); } // For vsync, we need to change the present mode, which means recreating the swap chain. diff --git a/Source/Core/VideoBackends/Vulkan/StateTracker.cpp b/Source/Core/VideoBackends/Vulkan/StateTracker.cpp index bfd198886b..c7c4c77c54 100644 --- a/Source/Core/VideoBackends/Vulkan/StateTracker.cpp +++ b/Source/Core/VideoBackends/Vulkan/StateTracker.cpp @@ -14,6 +14,7 @@ #include "VideoBackends/Vulkan/ObjectCache.h" #include "VideoBackends/Vulkan/StreamBuffer.h" #include "VideoBackends/Vulkan/Util.h" +#include "VideoBackends/Vulkan/VertexFormat.h" #include "VideoBackends/Vulkan/VulkanContext.h" #include "VideoCommon/GeometryShaderManager.h" @@ -116,6 +117,93 @@ bool StateTracker::Initialize() return true; } +void StateTracker::LoadPipelineUIDCache() +{ + class PipelineInserter final : public LinearDiskCacheReader + { + public: + explicit PipelineInserter(StateTracker* this_ptr_) : this_ptr(this_ptr_) {} + void Read(const SerializedPipelineUID& key, const u32* value, u32 value_size) + { + this_ptr->PrecachePipelineUID(key); + } + + private: + StateTracker* this_ptr; + }; + + std::string filename = g_object_cache->GetDiskCacheFileName("pipeline-uid"); + PipelineInserter inserter(this); + + // OpenAndRead calls Close() first, which will flush all data to disk when reloading. + // This assertion must hold true, otherwise data corruption will result. + m_uid_cache.OpenAndRead(filename, inserter); +} + +void StateTracker::AppendToPipelineUIDCache(const PipelineInfo& info) +{ + SerializedPipelineUID sinfo; + sinfo.blend_state_bits = info.blend_state.bits; + sinfo.rasterizer_state_bits = info.rasterization_state.bits; + sinfo.depth_stencil_state_bits = info.depth_stencil_state.bits; + sinfo.vertex_decl = m_pipeline_state.vertex_format->GetVertexDeclaration(); + sinfo.vs_uid = m_vs_uid; + sinfo.gs_uid = m_gs_uid; + sinfo.ps_uid = m_ps_uid; + sinfo.primitive_topology = info.primitive_topology; + + u32 dummy_value = 0; + m_uid_cache.Append(sinfo, &dummy_value, 1); +} + +bool StateTracker::PrecachePipelineUID(const SerializedPipelineUID& uid) +{ + PipelineInfo pinfo = {}; + + // Need to create the vertex declaration first, rather than deferring to when a game creates a + // vertex loader that uses this format, since we need it to create a pipeline. + pinfo.vertex_format = VertexFormat::GetOrCreateMatchingFormat(uid.vertex_decl); + pinfo.pipeline_layout = uid.ps_uid.GetUidData()->bounding_box ? + g_object_cache->GetBBoxPipelineLayout() : + g_object_cache->GetStandardPipelineLayout(); + pinfo.vs = g_object_cache->GetVertexShaderForUid(uid.vs_uid); + if (pinfo.vs == VK_NULL_HANDLE) + { + WARN_LOG(VIDEO, "Failed to get vertex shader from cached UID."); + return false; + } + if (!uid.gs_uid.GetUidData()->IsPassthrough()) + { + pinfo.gs = g_object_cache->GetGeometryShaderForUid(uid.gs_uid); + if (pinfo.gs == VK_NULL_HANDLE) + { + WARN_LOG(VIDEO, "Failed to get geometry shader from cached UID."); + return false; + } + } + pinfo.ps = g_object_cache->GetPixelShaderForUid(uid.ps_uid); + if (pinfo.ps == VK_NULL_HANDLE) + { + WARN_LOG(VIDEO, "Failed to get pixel shader from cached UID."); + return false; + } + pinfo.render_pass = m_load_render_pass; + pinfo.blend_state.bits = uid.blend_state_bits; + pinfo.rasterization_state.bits = uid.rasterizer_state_bits; + pinfo.depth_stencil_state.bits = uid.depth_stencil_state_bits; + pinfo.primitive_topology = uid.primitive_topology; + + VkPipeline pipeline = g_object_cache->GetPipeline(pinfo); + if (pipeline == VK_NULL_HANDLE) + { + WARN_LOG(VIDEO, "Failed to get pipeline from cached UID."); + return false; + } + + // We don't need to do anything with this pipeline, just make sure it exists. + return true; +} + void StateTracker::SetVertexBuffer(VkBuffer buffer, VkDeviceSize offset) { if (m_vertex_buffer == buffer && m_vertex_buffer_offset == offset) @@ -793,41 +881,54 @@ void StateTracker::EndClearRenderPass() EndRenderPass(); } +PipelineInfo StateTracker::GetAlphaPassPipelineConfig(const PipelineInfo& info) const +{ + PipelineInfo temp_info = info; + + // Skip depth writes for this pass. The results will be the same, so no + // point in overwriting depth values with the same value. + temp_info.depth_stencil_state.write_enable = VK_FALSE; + + // Only allow alpha writes, and disable blending. + temp_info.blend_state.blend_enable = VK_FALSE; + temp_info.blend_state.logic_op_enable = VK_FALSE; + temp_info.blend_state.write_mask = VK_COLOR_COMPONENT_A_BIT; + + return temp_info; +} + +VkPipeline StateTracker::GetPipelineAndCacheUID(const PipelineInfo& info) +{ + auto result = g_object_cache->GetPipelineWithCacheResult(info); + + // Add to the UID cache if it is a new pipeline. + if (!result.second) + AppendToPipelineUIDCache(info); + + return result.first; +} + bool StateTracker::UpdatePipeline() { // We need at least a vertex and fragment shader if (m_pipeline_state.vs == VK_NULL_HANDLE || m_pipeline_state.ps == VK_NULL_HANDLE) return false; - // Grab a new pipeline object, this can fail - if (m_dstalpha_mode != DSTALPHA_ALPHA_PASS) + // Grab a new pipeline object, this can fail. + // We have to use a different blend state for the alpha pass of the dstalpha fallback. + if (m_dstalpha_mode == DSTALPHA_ALPHA_PASS) { - m_pipeline_object = g_object_cache->GetPipeline(m_pipeline_state); - if (m_pipeline_object == VK_NULL_HANDLE) - return false; + // We need to retain the existing state, since we don't want to break the next draw. + PipelineInfo temp_info = GetAlphaPassPipelineConfig(m_pipeline_state); + m_pipeline_object = GetPipelineAndCacheUID(temp_info); } else { - // We need to make a few modifications to the pipeline object, but retain - // the existing state, since we don't want to break the next draw. - PipelineInfo temp_info = m_pipeline_state; - - // Skip depth writes for this pass. The results will be the same, so no - // point in overwriting depth values with the same value. - temp_info.depth_stencil_state.write_enable = VK_FALSE; - - // Only allow alpha writes, and disable blending. - temp_info.blend_state.blend_enable = VK_FALSE; - temp_info.blend_state.logic_op_enable = VK_FALSE; - temp_info.blend_state.write_mask = VK_COLOR_COMPONENT_A_BIT; - - m_pipeline_object = g_object_cache->GetPipeline(temp_info); - if (m_pipeline_object == VK_NULL_HANDLE) - return false; + m_pipeline_object = GetPipelineAndCacheUID(m_pipeline_state); } m_dirty_flags |= DIRTY_FLAG_PIPELINE_BINDING; - return true; + return m_pipeline_object != VK_NULL_HANDLE; } bool StateTracker::UpdateDescriptorSet() diff --git a/Source/Core/VideoBackends/Vulkan/StateTracker.h b/Source/Core/VideoBackends/Vulkan/StateTracker.h index b31adec6b5..a42a9dc72d 100644 --- a/Source/Core/VideoBackends/Vulkan/StateTracker.h +++ b/Source/Core/VideoBackends/Vulkan/StateTracker.h @@ -9,6 +9,7 @@ #include #include "Common/CommonTypes.h" +#include "Common/LinearDiskCache.h" #include "VideoBackends/Vulkan/Constants.h" #include "VideoBackends/Vulkan/ObjectCache.h" #include "VideoCommon/GeometryShaderGen.h" @@ -111,15 +112,22 @@ public: bool IsWithinRenderArea(s32 x, s32 y, u32 width, u32 height) const; -private: - bool Initialize(); + // Reloads the UID cache, ensuring all pipelines used by the game so far have been created. + void LoadPipelineUIDCache(); - // Check that the specified viewport is within the render area. - // If not, ends the render pass if it is a clear render pass. - bool IsViewportWithinRenderArea() const; - bool UpdatePipeline(); - bool UpdateDescriptorSet(); - void UploadAllConstants(); +private: + // Serialized version of PipelineInfo, used when loading/saving the pipeline UID cache. + struct SerializedPipelineUID + { + u64 blend_state_bits; + u32 rasterizer_state_bits; + u32 depth_stencil_state_bits; + PortableVertexDeclaration vertex_decl; + VertexShaderUid vs_uid; + GeometryShaderUid gs_uid; + PixelShaderUid ps_uid; + VkPrimitiveTopology primitive_topology; + }; enum DITRY_FLAG : u32 { @@ -140,6 +148,32 @@ private: DIRTY_FLAG_ALL_DESCRIPTOR_SETS = DIRTY_FLAG_VS_UBO | DIRTY_FLAG_GS_UBO | DIRTY_FLAG_PS_SAMPLERS | DIRTY_FLAG_PS_SSBO }; + + bool Initialize(); + + // Appends the specified pipeline info, combined with the UIDs stored in the class. + // The info is here so that we can store variations of a UID, e.g. blend state. + void AppendToPipelineUIDCache(const PipelineInfo& info); + + // Precaches a pipeline based on the UID information. + bool PrecachePipelineUID(const SerializedPipelineUID& uid); + + // Check that the specified viewport is within the render area. + // If not, ends the render pass if it is a clear render pass. + bool IsViewportWithinRenderArea() const; + + // Gets a pipeline state that can be used to draw the alpha pass with constant alpha enabled. + PipelineInfo GetAlphaPassPipelineConfig(const PipelineInfo& info) const; + + // Obtains a Vulkan pipeline object for the specified pipeline configuration. + // Also adds this pipeline configuration to the UID cache if it is not present already. + VkPipeline GetPipelineAndCacheUID(const PipelineInfo& info); + + bool UpdatePipeline(); + bool UpdateDescriptorSet(); + void UploadAllConstants(); + + // Which bindings/state has to be updated before the next draw. u32 m_dirty_flags = 0; // input assembly @@ -194,5 +228,11 @@ private: std::vector m_cpu_accesses_this_frame; std::vector m_scheduled_command_buffer_kicks; bool m_allow_background_execution = true; + + // Draw state cache on disk + // We don't actually use the value field here, instead we generate the shaders from the uid + // on-demand. If all goes well, it should hit the shader and Vulkan pipeline cache, therefore + // loading should be reasonably efficient. + LinearDiskCache m_uid_cache; }; } diff --git a/Source/Core/VideoBackends/Vulkan/VertexFormat.cpp b/Source/Core/VideoBackends/Vulkan/VertexFormat.cpp index 27c1d06199..ba67472394 100644 --- a/Source/Core/VideoBackends/Vulkan/VertexFormat.cpp +++ b/Source/Core/VideoBackends/Vulkan/VertexFormat.cpp @@ -53,6 +53,19 @@ VertexFormat::VertexFormat(const PortableVertexDeclaration& in_vtx_decl) SetupInputState(); } +VertexFormat* VertexFormat::GetOrCreateMatchingFormat(const PortableVertexDeclaration& decl) +{ + auto vertex_format_map = VertexLoaderManager::GetNativeVertexFormatMap(); + auto iter = vertex_format_map->find(decl); + if (iter == vertex_format_map->end()) + { + auto ipair = vertex_format_map->emplace(decl, std::make_unique(decl)); + iter = ipair.first; + } + + return static_cast(iter->second.get()); +} + void VertexFormat::MapAttributes() { m_num_attributes = 0; diff --git a/Source/Core/VideoBackends/Vulkan/VertexFormat.h b/Source/Core/VideoBackends/Vulkan/VertexFormat.h index 3614366e2f..ef2d31d748 100644 --- a/Source/Core/VideoBackends/Vulkan/VertexFormat.h +++ b/Source/Core/VideoBackends/Vulkan/VertexFormat.h @@ -16,6 +16,11 @@ class VertexFormat : public ::NativeVertexFormat public: VertexFormat(const PortableVertexDeclaration& in_vtx_decl); + // Creates or obtains a pointer to a VertexFormat representing decl. + // If this results in a VertexFormat being created, if the game later uses a matching vertex + // declaration, the one that was previously created will be used. + static VertexFormat* GetOrCreateMatchingFormat(const PortableVertexDeclaration& decl); + // Passed to pipeline state creation const VkPipelineVertexInputStateCreateInfo& GetVertexInputStateInfo() const {