diff --git a/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp b/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp index 61e685874a8e..761402e478e6 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(), @@ -330,6 +344,13 @@ bool ObjectCache::CreatePipelineCache(bool load_from_disk) disk_data.clear(); } + if (!disk_data.empty() && !ValidatePipelineCache(disk_data.data(), disk_data.size())) + { + // Don't use this data. In fact, we should delete it to prevent it from being used next time. + File::Delete(m_pipeline_cache_filename); + disk_data.clear(); + } + VkPipelineCacheCreateInfo info = { VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO, // VkStructureType sType nullptr, // const void* pNext @@ -355,6 +376,76 @@ bool ObjectCache::CreatePipelineCache(bool load_from_disk) return false; } +// Based on Vulkan 1.0 specification, +// Table 9.1. Layout for pipeline cache header version VK_PIPELINE_CACHE_HEADER_VERSION_ONE +// NOTE: This data is assumed to be in little-endian format. +#pragma pack(push, 4) +struct VK_PIPELINE_CACHE_HEADER +{ + u32 header_length; + u32 header_version; + u32 vendor_id; + u32 device_id; + u8 uuid[VK_UUID_SIZE]; +}; +#pragma pack(pop) +// TODO: Remove the #if here when GCC 5 is a minimum build requirement. +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 5 +static_assert(std::has_trivial_copy_constructor::value, + "VK_PIPELINE_CACHE_HEADER must be trivially copyable"); +#else +static_assert(std::is_trivially_copyable::value, + "VK_PIPELINE_CACHE_HEADER must be trivially copyable"); +#endif + +bool ObjectCache::ValidatePipelineCache(const u8* data, size_t data_length) +{ + if (data_length < sizeof(VK_PIPELINE_CACHE_HEADER)) + { + ERROR_LOG(VIDEO, "Pipeline cache failed validation: Invalid header"); + return false; + } + + VK_PIPELINE_CACHE_HEADER header; + std::memcpy(&header, data, sizeof(header)); + if (header.header_length < sizeof(VK_PIPELINE_CACHE_HEADER)) + { + ERROR_LOG(VIDEO, "Pipeline cache failed validation: Invalid header length"); + return false; + } + + if (header.header_version != VK_PIPELINE_CACHE_HEADER_VERSION_ONE) + { + ERROR_LOG(VIDEO, "Pipeline cache failed validation: Invalid header version"); + return false; + } + + if (header.vendor_id != g_vulkan_context->GetDeviceProperties().vendorID) + { + ERROR_LOG(VIDEO, + "Pipeline cache failed validation: Incorrect vendor ID (file: 0x%X, device: 0x%X)", + header.vendor_id, g_vulkan_context->GetDeviceProperties().vendorID); + return false; + } + + if (header.device_id != g_vulkan_context->GetDeviceProperties().deviceID) + { + ERROR_LOG(VIDEO, + "Pipeline cache failed validation: Incorrect device ID (file: 0x%X, device: 0x%X)", + header.device_id, g_vulkan_context->GetDeviceProperties().deviceID); + return false; + } + + if (std::memcmp(header.uuid, g_vulkan_context->GetDeviceProperties().pipelineCacheUUID, + VK_UUID_SIZE) != 0) + { + ERROR_LOG(VIDEO, "Pipeline cache failed validation: Incorrect UUID"); + return false; + } + + return true; +} + void ObjectCache::DestroyPipelineCache() { for (const auto& it : m_pipeline_objects) @@ -368,15 +459,6 @@ void ObjectCache::DestroyPipelineCache() m_pipeline_cache = VK_NULL_HANDLE; } -void ObjectCache::ClearPipelineCache() -{ - // Reallocate the pipeline cache object, so it starts fresh and we don't - // save old pipelines to disk. This is for major changes, e.g. MSAA mode change. - DestroyPipelineCache(); - if (!CreatePipelineCache(false)) - PanicAlert("Failed to re-create pipeline cache"); -} - void ObjectCache::SavePipelineCache() { size_t data_size; diff --git a/Source/Core/VideoBackends/Vulkan/ObjectCache.h b/Source/Core/VideoBackends/Vulkan/ObjectCache.h index b991b8a3b828..26593d139d97 100644 --- a/Source/Core/VideoBackends/Vulkan/ObjectCache.h +++ b/Source/Core/VideoBackends/Vulkan/ObjectCache.h @@ -111,12 +111,17 @@ class ObjectCache // 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); - // 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(); + // 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); // Saves the pipeline cache to disk. Call when shutting down. void SavePipelineCache(); @@ -133,8 +138,12 @@ class ObjectCache 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); + bool ValidatePipelineCache(const u8* data, size_t data_length); void DestroyPipelineCache(); void LoadShaderCaches(); void DestroyShaderCaches(); @@ -148,8 +157,6 @@ class ObjectCache 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 5348d6beec4e..1a82184fc4ff 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(); @@ -1134,8 +1137,8 @@ void Renderer::CheckForConfigChanges() g_command_buffer_mgr->WaitForGPUIdle(); RecompileShaders(); 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 bfd198886b20..c7c4c77c542e 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 b31adec6b51d..a42a9dc72d9a 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 @@ class StateTracker 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 @@ class StateTracker 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 @@ class StateTracker 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 27c1d06199b4..ba674723941d 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 3614366e2f25..ef2d31d7482f 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 {