Merge pull request #4449 from stenzek/vulkan-pipeline-cache

Vulkan: Implement pipeline UID cache
This commit is contained in:
Stenzek 2016-11-28 22:03:49 +10:00 committed by GitHub
commit da87580dc1
7 changed files with 304 additions and 53 deletions

View File

@ -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(),
@ -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<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)
@ -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;

View File

@ -111,12 +111,17 @@ 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);
// 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<VkPipeline, bool> GetPipelineWithCacheResult(const PipelineInfo& info);
// Saves the pipeline cache to disk. Call when shutting down.
void SavePipelineCache();
@ -133,8 +138,12 @@ 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);
bool ValidatePipelineCache(const u8* data, size_t data_length);
void DestroyPipelineCache();
void LoadShaderCaches();
void DestroyShaderCaches();
@ -148,8 +157,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;

View File

@ -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.

View File

@ -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()

View File

@ -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;
};
}

View File

@ -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;

View File

@ -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
{