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:
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user