diff --git a/Common/GPU/Vulkan/VulkanContext.cpp b/Common/GPU/Vulkan/VulkanContext.cpp index 4b5752ee23..46c4581779 100644 --- a/Common/GPU/Vulkan/VulkanContext.cpp +++ b/Common/GPU/Vulkan/VulkanContext.cpp @@ -13,6 +13,8 @@ #include "Common/GPU/Vulkan/VulkanContext.h" #include "Common/GPU/Vulkan/VulkanDebug.h" #include "Common/StringUtils.h" +#include "Common/File/FileUtil.h" +#include "ext/xxhash.h" #ifdef USE_CRT_DBG #undef new @@ -1778,8 +1780,180 @@ EShLanguage FindLanguage(const VkShaderStageFlagBits shader_type) { // Compile a given string containing GLSL into SPV for use by VK // Return value of false means an error was encountered. +// Bump when glslang or its options change, so that nothing it compiled differently is used. +static const uint32_t SPIRV_CACHE_VERSION = 2; +static const uint32_t SPIRV_CACHE_MAGIC = 0x43565053; // "SPVC" +static const uint32_t SPIRV_MAGIC = 0x07230203; + +struct SPIRVCacheHeader { + uint32_t magic; + uint32_t version; + uint32_t count; +}; + +struct SPIRVCacheEntryHeader { + uint32_t keyHash; + uint32_t keyLength; + uint32_t numWords; + uint32_t checksum; +}; + +SPIRVCache g_spirvCache; + +SPIRVCache::Key SPIRVCache::MakeKey(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source) { + const size_t length = strlen(source); + const uint32_t seed = (SPIRV_CACHE_VERSION << 16) | ((uint32_t)stage << 4) | (uint32_t)variant; + return Key{ XXH32(source, length, seed), (uint32_t)length }; +} + +bool SPIRVCache::Lookup(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source, std::vector *spirv) { + const Key key = MakeKey(stage, variant, source); + std::lock_guard guard(mutex_); + LoadIfNeededLocked(); + auto iter = entries_.find(key); + if (iter == entries_.end()) { + return false; + } + iter->second.used = true; + *spirv = iter->second.spirv; + return true; +} + +void SPIRVCache::Insert(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source, const std::vector &spirv) { + const Key key = MakeKey(stage, variant, source); + std::lock_guard guard(mutex_); + Entry &entry = entries_[key]; + entry.spirv = spirv; + entry.used = true; + dirty_ = true; +} + +void SPIRVCache::Clear() { + std::lock_guard guard(mutex_); + entries_.clear(); + dirty_ = false; +} + +bool SPIRVCache::Read(FILE *f) { + std::lock_guard guard(mutex_); + return ReadLocked(f); +} + +// A bad entry is dropped rather than failing the whole read: it's only a cache, and the shader +// gets compiled again. A truncated file keeps what came before the damage. +bool SPIRVCache::ReadLocked(FILE *f) { + entries_.clear(); + dirty_ = false; + SPIRVCacheHeader header{}; + if (fread(&header, sizeof(header), 1, f) != 1 || header.magic != SPIRV_CACHE_MAGIC) { + return false; + } + if (header.version != SPIRV_CACHE_VERSION) { + INFO_LOG(Log::G3D, "SPIR-V cache version %d, expected %d - starting over", header.version, SPIRV_CACHE_VERSION); + return false; + } + for (uint32_t i = 0; i < header.count; i++) { + SPIRVCacheEntryHeader entryHeader{}; + if (fread(&entryHeader, sizeof(entryHeader), 1, f) != 1 || entryHeader.numWords == 0 || entryHeader.numWords > 1024 * 1024) { + WARN_LOG(Log::G3D, "SPIR-V cache truncated or damaged after %d entries", i); + return false; + } + std::vector spirv(entryHeader.numWords); + if (fread(spirv.data(), sizeof(uint32_t), spirv.size(), f) != spirv.size()) { + WARN_LOG(Log::G3D, "SPIR-V cache truncated after %d entries", i); + return false; + } + if (spirv[0] != SPIRV_MAGIC || (uint32_t)XXH3_64bits(spirv.data(), spirv.size() * sizeof(uint32_t)) != entryHeader.checksum) { + WARN_LOG(Log::G3D, "Bad entry in SPIR-V cache, skipping"); + continue; + } + entries_[Key{ entryHeader.keyHash, entryHeader.keyLength }].spirv = std::move(spirv); + } + return true; +} + +bool SPIRVCache::Write(FILE *f, bool onlyUsed) { + std::lock_guard guard(mutex_); + SPIRVCacheHeader header{ SPIRV_CACHE_MAGIC, SPIRV_CACHE_VERSION, 0 }; + for (const auto &[key, entry] : entries_) { + if (entry.used || !onlyUsed) { + header.count++; + } + } + bool ok = fwrite(&header, sizeof(header), 1, f) == 1; + for (const auto &[key, entry] : entries_) { + if (!ok) { + break; + } + if (!entry.used && onlyUsed) { + continue; + } + SPIRVCacheEntryHeader entryHeader{}; + entryHeader.keyHash = key.hash; + entryHeader.keyLength = key.length; + entryHeader.numWords = (uint32_t)entry.spirv.size(); + entryHeader.checksum = (uint32_t)XXH3_64bits(entry.spirv.data(), entry.spirv.size() * sizeof(uint32_t)); + ok = fwrite(&entryHeader, sizeof(entryHeader), 1, f) == 1 && + fwrite(entry.spirv.data(), sizeof(uint32_t), entry.spirv.size(), f) == entry.spirv.size(); + } + if (ok) { + dirty_ = false; + } + return ok; +} + +void SPIRVCache::SetPath(const Path &path, int maxEntries) { + std::lock_guard guard(mutex_); + path_ = path; + maxEntries_ = maxEntries; + loaded_ = false; +} + +void SPIRVCache::LoadIfNeededLocked() { + if (loaded_ || path_.empty()) { + return; + } + loaded_ = true; + FILE *f = File::OpenCFile(path_, "rb"); + if (f) { + ReadLocked(f); + fclose(f); + INFO_LOG(Log::G3D, "Loaded %d shaders from the SPIR-V cache", (int)entries_.size()); + } + // Entries for shaders that have since changed pile up, so start over once there are a lot of + // them. A few shaders compiling once more doesn't matter. + if (maxEntries_ > 0 && (int)entries_.size() >= maxEntries_) { + INFO_LOG(Log::G3D, "SPIR-V cache has %d entries, flushing it", (int)entries_.size()); + entries_.clear(); + dirty_ = true; + } +} + +void SPIRVCache::SaveIfDirty() { + Path path; + { + std::lock_guard guard(mutex_); + if (!dirty_ || path_.empty()) { + return; + } + path = path_; + } + FILE *f = File::OpenCFile(path, "wb"); + if (!f) { + return; + } + // A small fixed set of shaders, so keep everything - including ones this run didn't need. + if (!Write(f, false)) { + ERROR_LOG(Log::G3D, "Failed to write the SPIR-V cache, disk full?"); + } + fclose(f); +} + bool GLSLtoSPV(const VkShaderStageFlagBits shader_type, const char *sourceCode, GLSLVariant variant, - std::vector &spirv, std::string *errorMessage) { + std::vector &spirv, std::string *errorMessage, SPIRVCache *cache) { + if (cache && cache->Lookup(shader_type, variant, sourceCode, &spirv)) { + return true; + } glslang::TProgram program; const char *shaderStrings[1]; @@ -1848,6 +2022,9 @@ bool GLSLtoSPV(const VkShaderStageFlagBits shader_type, const char *sourceCode, options.optimizeSize = false; options.generateDebugInfo = false; glslang::GlslangToSpv(*program.getIntermediate(stage), spirv, &options); + if (cache && !spirv.empty()) { + cache->Insert(shader_type, variant, sourceCode, spirv); + } return true; } diff --git a/Common/GPU/Vulkan/VulkanContext.h b/Common/GPU/Vulkan/VulkanContext.h index a8e9e7b03f..31a495ec32 100644 --- a/Common/GPU/Vulkan/VulkanContext.h +++ b/Common/GPU/Vulkan/VulkanContext.h @@ -9,9 +9,11 @@ #include #include #include +#include #include "Common/Common.h" #include "Common/Log.h" +#include "Common/File/Path.h" #include "Common/GPU/MiscTypes.h" #include "Common/GPU/Vulkan/VulkanLoader.h" #include "Common/GPU/Vulkan/VulkanDebug.h" @@ -613,7 +615,57 @@ enum class GLSLVariant { GLES300, }; -bool GLSLtoSPV(const VkShaderStageFlagBits shader_type, const char *sourceCode, GLSLVariant variant, std::vector &spirv, std::string *errorMessage); +// Compiled SPIR-V, keyed on the GLSL source, stage and variant, so that a shader compiled in an earlier +// run doesn't have to go through glslang again. Thread safe. +class SPIRVCache { +public: + bool Lookup(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source, std::vector *spirv); + void Insert(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source, const std::vector &spirv); + void Clear(); + + // For a cache stored inside another file. Read replaces the contents; Write can skip entries that + // haven't been looked up or inserted since, so that ones nothing uses anymore age out. + bool Read(FILE *f); + bool Write(FILE *f, bool onlyUsed); + + // For a cache with a file of its own, loaded on the first lookup. If it has grown to maxEntries, + // it's flushed on load and starts over. + void SetPath(const Path &path, int maxEntries); + void SaveIfDirty(); + +private: + // The source length along with the hash makes a collision, which would hand a shader the wrong + // SPIR-V, far less likely than a 32-bit hash alone. + struct Key { + uint32_t hash; + uint32_t length; + bool operator==(const Key &other) const { return hash == other.hash && length == other.length; } + }; + struct KeyHash { + size_t operator()(const Key &key) const { return key.hash; } + }; + struct Entry { + std::vector spirv; + bool used = false; + }; + static Key MakeKey(VkShaderStageFlagBits stage, GLSLVariant variant, const char *source); + bool ReadLocked(FILE *f); + void LoadIfNeededLocked(); + + std::mutex mutex_; + std::unordered_map entries_; + Path path_; + int maxEntries_ = 0; + bool loaded_ = false; + bool dirty_ = false; +}; + +// For thin3d's and other fixed shaders. Game shaders use a cache of their own, stored with the rest of +// the game's shader cache. +extern SPIRVCache g_spirvCache; + +// With a cache, a shader found there skips glslang, and a newly compiled one is added to it. +bool GLSLtoSPV(const VkShaderStageFlagBits shader_type, const char *sourceCode, GLSLVariant variant, std::vector &spirv, std::string *errorMessage, SPIRVCache *cache = nullptr); const char *VulkanColorSpaceToString(VkColorSpaceKHR colorSpace); const char *VulkanFormatToString(VkFormat format); diff --git a/Common/GPU/Vulkan/thin3d_vulkan.cpp b/Common/GPU/Vulkan/thin3d_vulkan.cpp index a724f61eaf..04cb77d5c1 100644 --- a/Common/GPU/Vulkan/thin3d_vulkan.cpp +++ b/Common/GPU/Vulkan/thin3d_vulkan.cpp @@ -219,7 +219,7 @@ bool VKShaderModule::Compile(VulkanContext *vulkan, const uint8_t *data, size_t source_ = (const char *)data; std::vector spirv; std::string errorMessage; - if (!GLSLtoSPV(vkstage_, source_.c_str(), GLSLVariant::VULKAN, spirv, &errorMessage)) { + if (!GLSLtoSPV(vkstage_, source_.c_str(), GLSLVariant::VULKAN, spirv, &errorMessage, &g_spirvCache)) { WARN_LOG(Log::G3D, "Shader compile to module failed (%s): %s", tag_.c_str(), errorMessage.c_str()); return false; } diff --git a/GPU/Vulkan/GPU_Vulkan.cpp b/GPU/Vulkan/GPU_Vulkan.cpp index 7f8c96095a..d8c63b7727 100644 --- a/GPU/Vulkan/GPU_Vulkan.cpp +++ b/GPU/Vulkan/GPU_Vulkan.cpp @@ -171,6 +171,9 @@ void GPU_Vulkan::SaveCache(const Path &filename) { pipelineManager_->SavePipelineCache(f, false, shaderManagerVulkan_, draw_); INFO_LOG(Log::G3D, "Saved Vulkan pipeline cache"); fclose(f); + + // And the fixed shaders the GPU compiled along the way. + g_spirvCache.SaveIfDirty(); } GPU_Vulkan::~GPU_Vulkan() { diff --git a/GPU/Vulkan/ShaderManagerVulkan.cpp b/GPU/Vulkan/ShaderManagerVulkan.cpp index 5ab295f5f6..31b040c816 100644 --- a/GPU/Vulkan/ShaderManagerVulkan.cpp +++ b/GPU/Vulkan/ShaderManagerVulkan.cpp @@ -40,14 +40,14 @@ // takes time here, and makes this worthy of parallelization, is GLSLtoSPV. // Takes ownership over tag. // This always returns something, checking the return value for null is not meaningful. -static Promise *CompileShaderModuleAsync(VulkanContext *vulkan, VkShaderStageFlagBits stage, const char *code, std::string *tag) { +static Promise *CompileShaderModuleAsync(VulkanContext *vulkan, VkShaderStageFlagBits stage, const char *code, std::string *tag, SPIRVCache *cache) { auto compile = [=] { PROFILE_THIS_SCOPE("shadercomp"); std::string errorMessage; std::vector spirv; - bool success = GLSLtoSPV(stage, code, GLSLVariant::VULKAN, spirv, &errorMessage); + bool success = GLSLtoSPV(stage, code, GLSLVariant::VULKAN, spirv, &errorMessage, cache); if (!errorMessage.empty()) { if (success) { @@ -102,11 +102,11 @@ static Promise *CompileShaderModuleAsync(VulkanContext *vulkan, } } -VulkanFragmentShader::VulkanFragmentShader(VulkanContext *vulkan, FShaderID id, FragmentShaderFlags flags, const char *code) +VulkanFragmentShader::VulkanFragmentShader(VulkanContext *vulkan, FShaderID id, FragmentShaderFlags flags, const char *code, SPIRVCache *cache) : vulkan_(vulkan), id_(id), flags_(flags) { _assert_(!id.is_invalid()); source_ = code; - module_ = CompileShaderModuleAsync(vulkan, VK_SHADER_STAGE_FRAGMENT_BIT, source_.c_str(), new std::string(id.Description())); + module_ = CompileShaderModuleAsync(vulkan, VK_SHADER_STAGE_FRAGMENT_BIT, source_.c_str(), new std::string(id.Description()), cache); VERBOSE_LOG(Log::G3D, "Compiled fragment shader:\n%s\n", (const char *)code); } @@ -133,11 +133,11 @@ std::string VulkanFragmentShader::GetShaderString(DebugShaderStringType type) co } } -VulkanVertexShader::VulkanVertexShader(VulkanContext *vulkan, VShaderID id, VertexShaderFlags flags, const char *code, bool useHWTransform) +VulkanVertexShader::VulkanVertexShader(VulkanContext *vulkan, VShaderID id, VertexShaderFlags flags, const char *code, bool useHWTransform, SPIRVCache *cache) : vulkan_(vulkan), useHWTransform_(useHWTransform), flags_(flags), id_(id) { _assert_(!id.is_invalid()); source_ = code; - module_ = CompileShaderModuleAsync(vulkan, VK_SHADER_STAGE_VERTEX_BIT, source_.c_str(), new std::string(id.Description())); + module_ = CompileShaderModuleAsync(vulkan, VK_SHADER_STAGE_VERTEX_BIT, source_.c_str(), new std::string(id.Description()), cache); VERBOSE_LOG(Log::G3D, "Compiled vertex shader:\n%s\n", (const char *)code); } @@ -245,7 +245,7 @@ const VulkanVertexShader *ShaderManagerVulkan::GetVertexShaderFromID(VShaderID V _assert_msg_(strlen(codeBuffer_) < CODE_BUFFER_SIZE, "VS length error: %d", (int)strlen(codeBuffer_)); const bool useHWTransform = VSID.Bit(VS_BIT_USE_HW_TRANSFORM); - vs = new VulkanVertexShader(vulkan, VSID, flags, codeBuffer_, useHWTransform); + vs = new VulkanVertexShader(vulkan, VSID, flags, codeBuffer_, useHWTransform, &spirvCache_); vsCache_.Insert(VSID, vs); return vs; } @@ -264,7 +264,7 @@ const VulkanFragmentShader *ShaderManagerVulkan::GetFragmentShaderFromID(FShader _assert_msg_(success, "FS gen error: %s", genErrorString.c_str()); _assert_msg_(strlen(codeBuffer_) < CODE_BUFFER_SIZE, "FS length error: %d", (int)strlen(codeBuffer_)); - fs = new VulkanFragmentShader(vulkan, FSID, flags, codeBuffer_); + fs = new VulkanFragmentShader(vulkan, FSID, flags, codeBuffer_, &spirvCache_); fsCache_.Insert(FSID, fs); return fs; } @@ -369,7 +369,7 @@ enum class VulkanCacheDetectFlags { }; #define CACHE_HEADER_MAGIC 0xff51f420 -#define CACHE_VERSION 59 +#define CACHE_VERSION 60 struct VulkanCacheHeader { uint32_t magic; @@ -413,6 +413,12 @@ bool ShaderManagerVulkan::LoadCache(FILE *f) { gstate_c.useFlagsChanged = false; } + // The SPIR-V comes first, so that compiling the shaders below mostly finds it there. + if (!spirvCache_.Read(f)) { + ERROR_LOG(Log::G3D, "Vulkan shader cache: SPIR-V missing or damaged"); + return false; + } + int failCount = 0; VulkanContext *vulkan = (VulkanContext *)draw_->GetNativeObject(Draw::NativeObject::CONTEXT); @@ -436,7 +442,7 @@ bool ShaderManagerVulkan::LoadCache(FILE *f) { _assert_msg_(strlen(codeBuffer_) < CODE_BUFFER_SIZE, "VS length error: %d", (int)strlen(codeBuffer_)); // Don't add the new shader if already compiled - though this should no longer happen. if (!vsCache_.ContainsKey(id)) { - VulkanVertexShader *vs = new VulkanVertexShader(vulkan, id, flags, codeBuffer_, useHWTransform); + VulkanVertexShader *vs = new VulkanVertexShader(vulkan, id, flags, codeBuffer_, useHWTransform, &spirvCache_); vsCache_.Insert(id, vs); } } @@ -459,7 +465,7 @@ bool ShaderManagerVulkan::LoadCache(FILE *f) { } _assert_msg_(strlen(codeBuffer_) < CODE_BUFFER_SIZE, "FS length error: %d", (int)strlen(codeBuffer_)); if (!fsCache_.ContainsKey(id)) { - VulkanFragmentShader *fs = new VulkanFragmentShader(vulkan, id, flags, codeBuffer_); + VulkanFragmentShader *fs = new VulkanFragmentShader(vulkan, id, flags, codeBuffer_, &spirvCache_); fsCache_.Insert(id, fs); } } @@ -478,6 +484,8 @@ void ShaderManagerVulkan::SaveCache(FILE *f, DrawEngineVulkan *drawEngine) { header.numFragmentShaders = (int)fsCache_.size(); header.unused_numGeometryShaders = 0; bool writeFailed = fwrite(&header, sizeof(header), 1, f) != 1; + // Only what this run compiled or used, so that SPIR-V of shaders that have since changed ages out. + writeFailed = writeFailed || !spirvCache_.Write(f, true); vsCache_.Iterate([&](const VShaderID &id, VulkanVertexShader *vs) { writeFailed = writeFailed || fwrite(&id, sizeof(id), 1, f) != 1; }); diff --git a/GPU/Vulkan/ShaderManagerVulkan.h b/GPU/Vulkan/ShaderManagerVulkan.h index 44d5f172e9..3ba870b94d 100644 --- a/GPU/Vulkan/ShaderManagerVulkan.h +++ b/GPU/Vulkan/ShaderManagerVulkan.h @@ -40,7 +40,7 @@ class VulkanPushPool; class VulkanFragmentShader { public: - VulkanFragmentShader(VulkanContext *vulkan, FShaderID id, FragmentShaderFlags flags, const char *code); + VulkanFragmentShader(VulkanContext *vulkan, FShaderID id, FragmentShaderFlags flags, const char *code, SPIRVCache *cache); ~VulkanFragmentShader(); const std::string &source() const { return source_; } @@ -63,7 +63,7 @@ protected: class VulkanVertexShader { public: - VulkanVertexShader(VulkanContext *vulkan, VShaderID id, VertexShaderFlags flags, const char *code, bool useHWTransform); + VulkanVertexShader(VulkanContext *vulkan, VShaderID id, VertexShaderFlags flags, const char *code, bool useHWTransform, SPIRVCache *cache); ~VulkanVertexShader(); const std::string &source() const { return source_; } @@ -145,6 +145,9 @@ private: char *codeBuffer_; + // The SPIR-V of this game's shaders, saved with the rest of its shader cache. + SPIRVCache spirvCache_; + uint64_t uboAlignment_; Uniforms *uniforms_; diff --git a/GPU/Vulkan/VulkanUtil.cpp b/GPU/Vulkan/VulkanUtil.cpp index ec90ad6242..4a951645d1 100644 --- a/GPU/Vulkan/VulkanUtil.cpp +++ b/GPU/Vulkan/VulkanUtil.cpp @@ -84,7 +84,7 @@ void InitVulkanCreateInfoFromConfig(VulkanContext::CreateInfo *info) { VkShaderModule CompileShaderModule(VulkanContext *vulkan, VkShaderStageFlagBits stage, const char *code, std::string *error) { std::vector spirv; - bool success = GLSLtoSPV(stage, code, GLSLVariant::VULKAN, spirv, error); + bool success = GLSLtoSPV(stage, code, GLSLVariant::VULKAN, spirv, error, &g_spirvCache); if (!error->empty()) { if (success) { ERROR_LOG(Log::G3D, "Warnings in shader compilation!"); diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 504890ff10..c8031da07a 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -62,6 +62,7 @@ #include "Common/Render/Text/draw_text.h" #include "Common/GPU/OpenGL/GLFeatures.h" #include "Common/GPU/thin3d.h" +#include "Common/GPU/Vulkan/VulkanContext.h" #include "Common/UI/UI.h" #include "Common/UI/Screen.h" #include "Common/UI/ScreenManager.h" @@ -798,6 +799,16 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO g_DownloadManager.SetCacheDir(GetSysDirectory(DIRECTORY_APP_CACHE)); +#if !PPSSPP_PLATFORM(UWP) + // The SPIR-V of thin3d's shaders and other fixed ones. Only read on first use, so only by Vulkan. + if (g_Config.bShaderCache) { + File::CreateFullPath(GetSysDirectory(DIRECTORY_APP_CACHE)); + // About twice what a session compiles (menu and a game: 10, a few more with post-processing + // or texture upscaling), so it's flushed once outdated entries have piled up. + g_spirvCache.SetPath(GetSysDirectory(DIRECTORY_APP_CACHE) / "vulkan_spirv.cache", 32); + } +#endif + ApplyAchievementsHostOverride(); g_screenManager = new ScreenManager(); @@ -966,6 +977,10 @@ bool NativeInitGraphics(GraphicsContext *graphicsContext) { ImGui_ImplThin3d_CreateDeviceObjects(g_draw); } +#if !PPSSPP_PLATFORM(UWP) + // Now, rather than only at shutdown: on mobile the app can be killed without one. + g_spirvCache.SaveIfDirty(); +#endif INFO_LOG(Log::System, "NativeInitGraphics completed"); @@ -1027,6 +1042,10 @@ bool CreateGlobalPipelines() { void NativeShutdownGraphics(GraphicsContext *graphicsContext) { INFO_LOG(Log::System, "NativeShutdownGraphics begin"); +#if !PPSSPP_PLATFORM(UWP) + g_spirvCache.SaveIfDirty(); +#endif + graphicsContext->NotifyEmuThreadExit(); if (g_screenManager) {