Vulkan: Cache compiled SPIR-V so shaders skip glslang on later runs

GLSLtoSPV takes an optional SPIRVCache, keyed on a 32-bit hash of the
source, stage and variant, plus the source length. A changed shader
simply misses. thin3d's shaders and the other fixed ones use a global
cache in PSP/SYSTEM/CACHE/vulkan_spirv.cache, loaded on first use and
saved after graphics init, when a game's cache is saved, and at
shutdown; it's flushed once it reaches 32 entries, about twice what a
session compiles, so outdated ones don't pile up. Game shaders keep
theirs in the .vkshadercache, ahead of the shader IDs so that the
compiles on load find it (version 60), and only what the session used
is saved.

A cold glslang costs about 40ms before its first shader here, and
0.3-0.9ms per shader after that.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-28 16:03:52 -06:00
1 parent 3970fa5c62
commit 8b4e93021e
8 files changed
+279 -17

No files matched your search

+3
View File
@@ -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() {
+19 -11
View File
@@ -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<VkShaderModule> *CompileShaderModuleAsync(VulkanContext *vulkan, VkShaderStageFlagBits stage, const char *code, std::string *tag) {
static Promise<VkShaderModule> *CompileShaderModuleAsync(VulkanContext *vulkan, VkShaderStageFlagBits stage, const char *code, std::string *tag, SPIRVCache *cache) {
auto compile = [=] {
PROFILE_THIS_SCOPE("shadercomp");
std::string errorMessage;
std::vector<uint32_t> 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<VkShaderModule> *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;
});
+5 -2
View File
@@ -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_;
+1 -1
View File
@@ -84,7 +84,7 @@ void InitVulkanCreateInfoFromConfig(VulkanContext::CreateInfo *info) {
VkShaderModule CompileShaderModule(VulkanContext *vulkan, VkShaderStageFlagBits stage, const char *code, std::string *error) {
std::vector<uint32_t> 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!");