From aac66a1b61a0e4c1c2b85d89bc818a8b521bcffa Mon Sep 17 00:00:00 2001
From: Stenzek <stenzek@gmail.com>
Date: Sun, 13 Nov 2016 18:39:06 +1000
Subject: [PATCH 1/3] 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<VkPipeline, bool> 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<VkPipeline, bool> 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<VkDescriptorSetLayout, NUM_DESCRIPTOR_SETS> 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<SerializedPipelineUID, u32>
+  {
+  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 <memory>
 
 #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<u32> m_cpu_accesses_this_frame;
   std::vector<u32> 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<SerializedPipelineUID, u32> 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<VertexFormat>(decl));
+    iter = ipair.first;
+  }
+
+  return static_cast<VertexFormat*>(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
   {

From 9604b336c8213c43dff90fab67df8eaa31cdbbb5 Mon Sep 17 00:00:00 2001
From: Stenzek <stenzek@gmail.com>
Date: Sun, 13 Nov 2016 18:41:36 +1000
Subject: [PATCH 2/3] Vulkan: Don't destroy the device's pipeline cache on MSAA
 mode change

The user could switch back again, and this would mean this data would be
lost. Disk space is cheap, and it's not going to be much.
---
 Source/Core/VideoBackends/Vulkan/ObjectCache.cpp | 9 ---------
 Source/Core/VideoBackends/Vulkan/ObjectCache.h   | 4 ----
 Source/Core/VideoBackends/Vulkan/Renderer.cpp    | 1 -
 3 files changed, 14 deletions(-)

diff --git a/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp b/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp
index ecfc73c76e..d7d505b793 100644
--- a/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp
+++ b/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp
@@ -382,15 +382,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 e102440164..a1b9be54a4 100644
--- a/Source/Core/VideoBackends/Vulkan/ObjectCache.h
+++ b/Source/Core/VideoBackends/Vulkan/ObjectCache.h
@@ -123,10 +123,6 @@ public:
   // otherwise for a cache hit it will be true.
   std::pair<VkPipeline, bool> 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();
-
   // Saves the pipeline cache to disk. Call when shutting down.
   void SavePipelineCache();
 
diff --git a/Source/Core/VideoBackends/Vulkan/Renderer.cpp b/Source/Core/VideoBackends/Vulkan/Renderer.cpp
index 05ae219ec2..1a82184fc4 100644
--- a/Source/Core/VideoBackends/Vulkan/Renderer.cpp
+++ b/Source/Core/VideoBackends/Vulkan/Renderer.cpp
@@ -1137,7 +1137,6 @@ void Renderer::CheckForConfigChanges()
     g_command_buffer_mgr->WaitForGPUIdle();
     RecompileShaders();
     FramebufferManager::GetInstance()->RecompileShaders();
-    g_object_cache->ClearPipelineCache();
     g_object_cache->RecompileSharedShaders();
     StateTracker::GetInstance()->LoadPipelineUIDCache();
   }

From 8d48319414d7e3f348e4ef38e976121e9a1fce6a Mon Sep 17 00:00:00 2001
From: Stenzek <stenzek@gmail.com>
Date: Sun, 13 Nov 2016 18:50:10 +1000
Subject: [PATCH 3/3] Vulkan: Validate the pipeline cache before using it

This ensures that if a user changes adapters or vendors we're not passing
invalid data to the driver.
---
 .../Core/VideoBackends/Vulkan/ObjectCache.cpp | 77 +++++++++++++++++++
 .../Core/VideoBackends/Vulkan/ObjectCache.h   |  1 +
 2 files changed, 78 insertions(+)

diff --git a/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp b/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp
index d7d505b793..761402e478 100644
--- a/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp
+++ b/Source/Core/VideoBackends/Vulkan/ObjectCache.cpp
@@ -344,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
@@ -369,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<VK_PIPELINE_CACHE_HEADER>::value,
+              "VK_PIPELINE_CACHE_HEADER must be trivially copyable");
+#else
+static_assert(std::is_trivially_copyable<VK_PIPELINE_CACHE_HEADER>::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)
diff --git a/Source/Core/VideoBackends/Vulkan/ObjectCache.h b/Source/Core/VideoBackends/Vulkan/ObjectCache.h
index a1b9be54a4..26593d139d 100644
--- a/Source/Core/VideoBackends/Vulkan/ObjectCache.h
+++ b/Source/Core/VideoBackends/Vulkan/ObjectCache.h
@@ -143,6 +143,7 @@ public:
 
 private:
   bool CreatePipelineCache(bool load_from_disk);
+  bool ValidatePipelineCache(const u8* data, size_t data_length);
   void DestroyPipelineCache();
   void LoadShaderCaches();
   void DestroyShaderCaches();