Vulkan: SPIR-V Caching

Adds a SPIR-V cache that skips frontend compilation for shaders
that are already compiled in a previous run of Blender.

Initially this was postponed to 4.4 but it was observed that
the vulkan backend didn't perform well on Windows in debug
builds. The reason is that the compiler would also be a debug
build which makes compiling a shader really slow. Starting
Blender on a debug build could take minutes.

So the decision was made to give this task a higher priority so
the vulkan backend would become more usable to developers
as well.

The cache is stored in the application cache dir. The SPIR-V
binaries can be used by different Blender versions so there
is no version specific cache folder.

**Sidecar**: SPIR-V files are a stream of bytes. There is no
header information that allow us to validate the stream. To
add basic validations we could add our custom header or
a sidecar. It was chosen to use a sidecar as having the SPIR-V
files unmodified allows us to load them directly in
debug tools for analyzing.

**Retention**: Shaders that are not used are automatically
removed with a retention period of 30 days.

**Shader builder**: Shader builder cannot use the SPIR-V
cache as it uses stubs that returns invalid cache directories.
This would load/save the cache to the location where you
started the build.

Pull Request: https://projects.blender.org/blender/blender/pulls/128741
This commit is contained in:
Jeroen Bakker
2024-10-08 10:55:10 +02:00
parent 3dd20a64f0
commit 3cd579208b
7 changed files with 168 additions and 9 deletions

View File

@@ -75,7 +75,10 @@ class VKBackend : public GPUBackend {
StorageBuf *storagebuf_alloc(size_t size, GPUUsageType usage, const char *name) override;
VertBuf *vertbuf_alloc() override;
void shader_cache_dir_clear_old() override {}
void shader_cache_dir_clear_old() override
{
VKShaderCompiler::cache_dir_clear_old();
}
/* Render Frame Coordination --
* Used for performing per-frame actions globally */

View File

@@ -640,6 +640,7 @@ bool VKShader::finalize_shader_module(VKShaderModule &shader_module, const char
shader_module.combined_sources.clear();
shader_module.sources_hash.clear();
shader_module.compilation_result = {};
shader_module.spirv_binary.clear();
return compilation_succeeded;
}

View File

@@ -131,8 +131,6 @@ class VKShader : public Shader {
}
private:
Vector<uint32_t> compile_glsl_to_spirv(Span<const char *> sources, shaderc_shader_kind kind);
void build_shader_module(Span<uint32_t> spirv_module, VKShaderModule &r_shader_module);
void build_shader_module(MutableSpan<const char *> sources,
shaderc_shader_kind stage,
VKShaderModule &r_shader_module);

View File

@@ -7,14 +7,17 @@
*/
#include "BKE_appdir.hh"
#include "BLI_fileops.hh"
#include "BLI_hash.hh"
#include "BLI_path_utils.hh"
#include "BLI_time.h"
#include "vk_shader_compiler.hh"
#ifdef _WIN32
# include "BLI_winstuff.h"
#endif
#include "vk_shader.hh"
#include "vk_shader_compiler.hh"
namespace blender::gpu {
VKShaderCompiler::VKShaderCompiler()
@@ -29,6 +32,122 @@ VKShaderCompiler::~VKShaderCompiler()
task_pool_ = nullptr;
}
/* -------------------------------------------------------------------- */
/** \name SPIR-V disk cache
* \{ */
struct SPIRVSidecar {
/** Size of the SPIRV binary. */
uint64_t spirv_size;
};
static std::optional<std::string> cache_dir_get()
{
char tmp_dir_buffer[FILE_MAX];
/* Shader builder doesn't return the correct appdir*/
if (!BKE_appdir_folder_caches(tmp_dir_buffer, sizeof(tmp_dir_buffer))) {
return std::nullopt;
}
std::string cache_dir = std::string(tmp_dir_buffer) + "vk-spirv-cache" + SEP_STR;
BLI_dir_create_recursive(cache_dir.c_str());
return cache_dir;
}
static bool read_spirv_from_disk(VKShaderModule &shader_module)
{
if (G.debug & G_DEBUG_GPU_RENDERDOC) {
/* Renderdoc uses spirv shaders including debug information. */
return false;
}
std::optional<std::string> cache_dir = cache_dir_get();
if (!cache_dir.has_value()) {
return false;
}
shader_module.build_sources_hash();
std::string spirv_path = (*cache_dir) + SEP_STR + shader_module.sources_hash + ".spv";
std::string sidecar_path = (*cache_dir) + SEP_STR + shader_module.sources_hash + ".sidecar.bin";
if (!BLI_exists(spirv_path.c_str()) || !BLI_exists(sidecar_path.c_str())) {
return false;
}
BLI_file_touch(spirv_path.c_str());
BLI_file_touch(sidecar_path.c_str());
/* Read sidecar*/
fstream sidecar_file(sidecar_path, std::ios::binary | std::ios::in | std::ios::ate);
std::streamsize sidecar_size_on_disk = sidecar_file.tellg();
SPIRVSidecar sidecar = {};
if (sidecar_size_on_disk != sizeof(sidecar)) {
return false;
}
sidecar_file.seekg(0, std::ios::beg);
sidecar_file.read(reinterpret_cast<char *>(&sidecar), sizeof(sidecar));
/* Read spirv binary */
fstream spirv_file(spirv_path, std::ios::binary | std::ios::in | std::ios::ate);
std::streamsize size = spirv_file.tellg();
if (size != sidecar.spirv_size) {
return false;
}
spirv_file.seekg(0, std::ios::beg);
shader_module.spirv_binary.resize(size / 4);
spirv_file.read(reinterpret_cast<char *>(shader_module.spirv_binary.data()), size);
return true;
}
static void write_spirv_to_disk(VKShaderModule &shader_module)
{
if (G.debug & G_DEBUG_GPU_RENDERDOC) {
return;
}
std::optional<std::string> cache_dir = cache_dir_get();
if (!cache_dir.has_value()) {
return;
}
/* Write the spirv binary */
std::string spirv_path = (*cache_dir) + SEP_STR + shader_module.sources_hash + ".spv";
size_t size = (shader_module.compilation_result.end() -
shader_module.compilation_result.begin()) *
sizeof(uint32_t);
fstream spirv_file(spirv_path, std::ios::binary | std::ios::out);
spirv_file.write(reinterpret_cast<const char *>(shader_module.compilation_result.begin()), size);
/* Write the sidecar */
SPIRVSidecar sidecar = {size};
std::string sidecar_path = (*cache_dir) + SEP_STR + shader_module.sources_hash + ".sidecar.bin";
fstream sidecar_file(sidecar_path, std::ios::binary | std::ios::out);
sidecar_file.write(reinterpret_cast<const char *>(&sidecar), sizeof(SPIRVSidecar));
}
void VKShaderCompiler::cache_dir_clear_old()
{
std::optional<std::string> cache_dir = cache_dir_get();
if (!cache_dir.has_value()) {
return;
}
direntry *entries = nullptr;
uint32_t dir_len = BLI_filelist_dir_contents(cache_dir->c_str(), &entries);
for (int i : blender::IndexRange(dir_len)) {
direntry entry = entries[i];
if (S_ISDIR(entry.s.st_mode)) {
continue;
}
const time_t ts_now = time(nullptr);
const time_t delete_threshold = 60 /*seconds*/ * 60 /*minutes*/ * 24 /*hours*/ * 30 /*days*/;
if (entry.s.st_mtime + delete_threshold < ts_now) {
BLI_delete(entry.path, false, false);
}
}
BLI_filelist_free(entries, dir_len);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Compilation
* \{ */
@@ -73,6 +192,10 @@ static bool compile_ex(shaderc::Compiler &compiler,
shaderc_shader_kind stage,
VKShaderModule &shader_module)
{
if (read_spirv_from_disk(shader_module)) {
return true;
}
shaderc::CompileOptions options;
options.SetOptimizationLevel(shaderc_optimization_level_performance);
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2);
@@ -86,6 +209,9 @@ static bool compile_ex(shaderc::Compiler &compiler,
shader_module.combined_sources, stage, full_name.c_str(), options);
bool compilation_succeeded = shader_module.compilation_result.GetCompilationStatus() ==
shaderc_compilation_status_success;
if (compilation_succeeded) {
write_spirv_to_disk(shader_module);
}
return compilation_succeeded;
}

View File

@@ -49,6 +49,8 @@ class VKShaderCompiler : public ShaderCompiler {
shaderc_shader_kind stage,
VKShaderModule &shader_module);
static void cache_dir_clear_old();
private:
static void run(TaskPool *__restrict pool, void *task_data);
};

View File

@@ -11,6 +11,9 @@
#include "vk_memory.hh"
#include "vk_shader.hh"
#include <iomanip>
#include <sstream>
namespace blender::gpu {
VKShaderModule::~VKShaderModule()
{
@@ -25,7 +28,9 @@ VKShaderModule::~VKShaderModule()
void VKShaderModule::finalize(StringRefNull name)
{
BLI_assert(vk_shader_module == VK_NULL_HANDLE);
if (compilation_result.GetCompilationStatus() != shaderc_compilation_status_success) {
if (compilation_result.GetCompilationStatus() != shaderc_compilation_status_success &&
spirv_binary.is_empty())
{
return;
}
@@ -33,9 +38,15 @@ void VKShaderModule::finalize(StringRefNull name)
VkShaderModuleCreateInfo create_info = {};
create_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
create_info.codeSize = (compilation_result.end() - compilation_result.begin()) *
sizeof(uint32_t);
create_info.pCode = compilation_result.begin();
if (!spirv_binary.is_empty()) {
create_info.codeSize = spirv_binary.size() * sizeof(uint32_t);
create_info.pCode = spirv_binary.data();
}
else {
create_info.codeSize = (compilation_result.end() - compilation_result.begin()) *
sizeof(uint32_t);
create_info.pCode = compilation_result.begin();
}
const VKDevice &device = VKBackend::get().device;
vkCreateShaderModule(
@@ -43,4 +54,15 @@ void VKShaderModule::finalize(StringRefNull name)
debug::object_label(vk_shader_module, name.c_str());
}
void VKShaderModule::build_sources_hash()
{
DefaultHash<std::string> hasher;
BLI_assert(!combined_sources.empty());
uint64_t hash = hasher(combined_sources);
std::stringstream ss;
ss << std::setfill('0') << std::setw(sizeof(uint64_t) * 2) << std::hex << hash;
sources_hash = ss.str();
BLI_assert(!sources_hash.empty());
}
} // namespace blender::gpu

View File

@@ -40,6 +40,9 @@ class VKShaderModule {
*/
std::string combined_sources;
/**
* Hash of the combined sources. Used to generate the name inside spirv cache.
*/
std::string sources_hash;
/**
@@ -53,6 +56,7 @@ class VKShaderModule {
* Is cleared after compilation phase has completed. (VKShader::finalize_post).
*/
shaderc::SpvCompilationResult compilation_result;
Vector<uint32_t> spirv_binary;
/**
* Is compilation needed and is the compilation step done.
@@ -71,6 +75,9 @@ class VKShaderModule {
* `vk_shader_module`.
*/
void finalize(StringRefNull name);
/** Build the sources hash from the combined_sources. */
void build_sources_hash();
};
} // namespace blender::gpu