diff --git a/Makefile.common b/Makefile.common index c5252d750395..393b41b769c0 100644 --- a/Makefile.common +++ b/Makefile.common @@ -1853,6 +1853,7 @@ ifeq ($(HAVE_SLANG),1) DEFINES += -DHAVE_SLANG OBJ += gfx/drivers_shader/slang_process.o OBJ += gfx/drivers_shader/glslang_util.o + OBJ += gfx/drivers_shader/slang_cache.o endif ifeq ($(HAVE_SHADERS_COMMON), 1) diff --git a/gfx/drivers_shader/slang_cache.cpp b/gfx/drivers_shader/slang_cache.cpp new file mode 100644 index 000000000000..79aa5c257f42 --- /dev/null +++ b/gfx/drivers_shader/slang_cache.cpp @@ -0,0 +1,351 @@ +#include "slang_cache.h" +#include "glslang_util.h" +#include "slang_process.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../configuration.h" +#include "../../verbosity.h" + +#define SPIRV_CACHE_VERSION 1 +#define SPIRV_CACHE_SUBDIR "spirv" + +/** + * Get the full path to the SPIR-V cache directory + * + * @param cache_dir_out Output buffer for the cache directory path (must be at least PATH_MAX_LENGTH) + * @param cache_dir_out_len Size of the output buffer + * @return true on success, false if cache dir is not configured + */ +static bool spirv_cache_get_dir(char *cache_dir_out, size_t cache_dir_out_len) +{ + settings_t *settings = config_get_ptr(); + + if (!settings || !settings->paths.directory_cache[0]) + return false; + + /* Build the spirv subdirectory path */ + snprintf(cache_dir_out, cache_dir_out_len, "%s/%s", + settings->paths.directory_cache, SPIRV_CACHE_SUBDIR); + + return true; +} + +/** + * Ensure the SPIR-V cache directory exists + * + * @return true if directory exists or was created, false on error + */ +static bool spirv_cache_ensure_dir(void) +{ + char cache_dir[PATH_MAX_LENGTH]; + + if (!spirv_cache_get_dir(cache_dir, sizeof(cache_dir))) + return false; + + return path_mkdir(cache_dir); +} + +/** + * Get the full path to a cache file for a given hash + * + * @param hash Hash string (64 characters) + * @param cache_file_out Output buffer for the full cache file path + * @param cache_file_out_len Size of the output buffer + * @return true on success, false on error + */ +static bool spirv_cache_get_filename(const char *hash, + char *cache_file_out, size_t cache_file_out_len) +{ + char cache_dir[PATH_MAX_LENGTH]; + + if (!spirv_cache_get_dir(cache_dir, sizeof(cache_dir))) + return false; + + snprintf(cache_file_out, cache_file_out_len, "%s/%s.spirv", + cache_dir, hash); + + return true; +} + +/** + * Write a null-terminated string to a file with length prefix + * + * @param file File pointer (opened in binary mode) + * @param str String to write (may be NULL or empty) + * @return true on success, false on error + */ +static bool spirv_cache_write_string(RFILE *file, const std::string &str) +{ + uint32_t len = str.length(); + + if (filestream_write(file, &len, sizeof(uint32_t)) != sizeof(uint32_t)) + return false; + + if (len > 0 && filestream_write(file, str.c_str(), len) != len) + return false; + + return true; +} + +/** + * Read a null-terminated string from a file + * + * @param file File pointer (opened in binary mode) + * @param str_out Output string + * @return true on success, false on error + */ +static bool spirv_cache_read_string(RFILE *file, std::string &str_out) +{ + uint32_t len; + + if (filestream_read(file, &len, sizeof(uint32_t)) != sizeof(uint32_t)) + return false; + + if (len == 0) + { + str_out.clear(); + return true; + } + + /* Allocate and read string */ + char *buf = new char[len + 1]; + if (!buf) + return false; + + if (filestream_read(file, buf, len) != len) + { + delete[] buf; + return false; + } + + buf[len] = '\0'; + str_out = buf; + delete[] buf; + + return true; +} + +extern "C" { + +bool spirv_cache_compute_hash(const char *vertex_source, const char *fragment_source, + char *hash_out) +{ + if (!vertex_source || !fragment_source || !hash_out) + return false; + + /* Build combined hash input: vertex + "|" + fragment */ + size_t vertex_len = strlen(vertex_source); + size_t fragment_len = strlen(fragment_source); + size_t total_len = vertex_len + 1 + fragment_len; /* 1 for "|" separator */ + + uint8_t *combined = new uint8_t[total_len]; + if (!combined) + return false; + + memcpy(combined, vertex_source, vertex_len); + combined[vertex_len] = '|'; + memcpy(combined + vertex_len + 1, fragment_source, fragment_len); + + /* Compute SHA256 hash using libretro-common */ + sha256_hash(hash_out, combined, total_len); + + delete[] combined; + return true; +} + +bool spirv_cache_load(const char *hash, struct glslang_output *output) +{ + RFILE *file; + char cache_file[PATH_MAX_LENGTH]; + uint8_t version; + uint32_t vertex_size, fragment_size, param_count, i; + uint16_t rt_format; + + if (!hash || !output) + return false; + + if (!spirv_cache_get_filename(hash, cache_file, sizeof(cache_file))) + return false; + + file = filestream_open(cache_file, RETRO_VFS_FILE_ACCESS_READ, + RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (!file) + return false; /* Cache file doesn't exist yet */ + + /* Read version */ + if (filestream_read(file, &version, sizeof(uint8_t)) != sizeof(uint8_t)) + goto error; + + if (version != SPIRV_CACHE_VERSION) + goto error; /* Version mismatch */ + + /* Read vertex SPIR-V */ + if (filestream_read(file, &vertex_size, sizeof(uint32_t)) != sizeof(uint32_t)) + goto error; + + if (vertex_size > 0) + { + output->vertex.resize(vertex_size); + if (filestream_read(file, output->vertex.data(), vertex_size * sizeof(uint32_t)) != (int64_t)(vertex_size * sizeof(uint32_t))) + goto error; + } + + /* Read fragment SPIR-V */ + if (filestream_read(file, &fragment_size, sizeof(uint32_t)) != sizeof(uint32_t)) + goto error; + + if (fragment_size > 0) + { + output->fragment.resize(fragment_size); + if (filestream_read(file, output->fragment.data(), fragment_size * sizeof(uint32_t)) != (int64_t)(fragment_size * sizeof(uint32_t))) + goto error; + } + + /* Read parameters count */ + if (filestream_read(file, ¶m_count, sizeof(uint32_t)) != sizeof(uint32_t)) + goto error; + + if (param_count > 0) + output->meta.parameters.resize(param_count); + + /* Read each parameter */ + for (i = 0; i < param_count; i++) + { + glslang_parameter ¶m = output->meta.parameters[i]; + + if (!spirv_cache_read_string(file, param.id)) + goto error; + + if (!spirv_cache_read_string(file, param.desc)) + goto error; + + if (filestream_read(file, ¶m.initial, sizeof(float)) != sizeof(float)) + goto error; + if (filestream_read(file, ¶m.minimum, sizeof(float)) != sizeof(float)) + goto error; + if (filestream_read(file, ¶m.maximum, sizeof(float)) != sizeof(float)) + goto error; + if (filestream_read(file, ¶m.step, sizeof(float)) != sizeof(float)) + goto error; + } + + /* Read shader name */ + if (!spirv_cache_read_string(file, output->meta.name)) + goto error; + + /* Read render target format */ + if (filestream_read(file, &rt_format, sizeof(uint16_t)) != sizeof(uint16_t)) + goto error; + output->meta.rt_format = (enum glslang_format)rt_format; + + filestream_close(file); + + RARCH_LOG("[Slang Cache] Loaded shader cache for hash: %.16s...\n", hash); + + return true; + +error: + filestream_close(file); + return false; +} + +bool spirv_cache_save(const char *hash, const struct glslang_output *output) +{ + RFILE *file; + char cache_file[PATH_MAX_LENGTH]; + uint8_t version = SPIRV_CACHE_VERSION; + uint32_t vertex_size, fragment_size, param_count, i; + uint16_t rt_format; + + if (!hash || !output) + return false; + + /* Ensure cache directory exists */ + if (!spirv_cache_ensure_dir()) + return false; + + if (!spirv_cache_get_filename(hash, cache_file, sizeof(cache_file))) + return false; + + file = filestream_open(cache_file, RETRO_VFS_FILE_ACCESS_WRITE, + RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (!file) + return false; + + /* Write version */ + if (filestream_write(file, &version, sizeof(uint8_t)) != sizeof(uint8_t)) + goto error; + + /* Write vertex SPIR-V */ + vertex_size = output->vertex.size(); + if (filestream_write(file, &vertex_size, sizeof(uint32_t)) != sizeof(uint32_t)) + goto error; + if (vertex_size > 0) + { + if (filestream_write(file, output->vertex.data(), vertex_size * sizeof(uint32_t)) != (int64_t)(vertex_size * sizeof(uint32_t))) + goto error; + } + + /* Write fragment SPIR-V */ + fragment_size = output->fragment.size(); + if (filestream_write(file, &fragment_size, sizeof(uint32_t)) != sizeof(uint32_t)) + goto error; + if (fragment_size > 0) + { + if (filestream_write(file, output->fragment.data(), fragment_size * sizeof(uint32_t)) != (int64_t)(fragment_size * sizeof(uint32_t))) + goto error; + } + + /* Write parameters */ + param_count = output->meta.parameters.size(); + if (filestream_write(file, ¶m_count, sizeof(uint32_t)) != sizeof(uint32_t)) + goto error; + + for (i = 0; i < param_count; i++) + { + const glslang_parameter ¶m = output->meta.parameters[i]; + + if (!spirv_cache_write_string(file, param.id)) + goto error; + if (!spirv_cache_write_string(file, param.desc)) + goto error; + + if (filestream_write(file, ¶m.initial, sizeof(float)) != sizeof(float)) + goto error; + if (filestream_write(file, ¶m.minimum, sizeof(float)) != sizeof(float)) + goto error; + if (filestream_write(file, ¶m.maximum, sizeof(float)) != sizeof(float)) + goto error; + if (filestream_write(file, ¶m.step, sizeof(float)) != sizeof(float)) + goto error; + } + + /* Write shader name */ + if (!spirv_cache_write_string(file, output->meta.name)) + goto error; + + /* Write render target format */ + rt_format = (uint16_t)output->meta.rt_format; + if (filestream_write(file, &rt_format, sizeof(uint16_t)) != sizeof(uint16_t)) + goto error; + + filestream_close(file); + + RARCH_LOG("[Slang Cache] Saved shader cache for hash: %.16s...\n", hash); + + return true; + +error: + filestream_close(file); + filestream_delete(cache_file); /* Clean up partial file on error */ + return false; +} + +} /* extern "C" */ diff --git a/gfx/drivers_shader/slang_cache.h b/gfx/drivers_shader/slang_cache.h new file mode 100644 index 000000000000..d813425a73e8 --- /dev/null +++ b/gfx/drivers_shader/slang_cache.h @@ -0,0 +1,46 @@ +#ifndef SLANG_CACHE_H +#define SLANG_CACHE_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Forward declaration of glslang_output structure */ +struct glslang_output; + +/** + * Compute SHA256 hash of preprocessed shader source code + * + * @param vertex_source Preprocessed vertex stage source (null-terminated string) + * @param fragment_source Preprocessed fragment stage source (null-terminated string) + * @param hash_out Output buffer for hex-encoded hash (must be at least 65 bytes: 64 hex chars + null terminator) + * @return true on success, false on error + */ +bool spirv_cache_compute_hash(const char *vertex_source, const char *fragment_source, char *hash_out); + +/** + * Load cached SPIR-V output from disk + * + * @param hash Hex-encoded SHA256 hash (64 characters) + * @param output Output structure to populate with cached data + * @return true if cache hit and successfully loaded, false otherwise + */ +bool spirv_cache_load(const char *hash, struct glslang_output *output); + +/** + * Save compiled SPIR-V output to disk cache + * + * @param hash Hex-encoded SHA256 hash (64 characters) + * @param output Compiled output structure to cache + * @return true on success, false on error + */ +bool spirv_cache_save(const char *hash, const struct glslang_output *output); + +#ifdef __cplusplus +} +#endif + +#endif /* SLANG_CACHE_H */ diff --git a/gfx/drivers_shader/slang_process.cpp b/gfx/drivers_shader/slang_process.cpp index 4ace77b81e7b..c0205115d319 100644 --- a/gfx/drivers_shader/slang_process.cpp +++ b/gfx/drivers_shader/slang_process.cpp @@ -26,6 +26,7 @@ #include "glslang_util.h" #if defined(HAVE_GLSLANG) #include "glslang.hpp" +#include "slang_cache.h" #endif #include "spirv_cross.hpp" #include "slang_process.h" @@ -733,6 +734,7 @@ bool glslang_compile_shader(const char *shader_path, glslang_output *output) { #if defined(HAVE_GLSLANG) struct shader_line_buf lines; + char cache_filename[PATH_MAX_LENGTH]; if (!shader_line_buf_init(&lines)) return false; @@ -741,6 +743,23 @@ bool glslang_compile_shader(const char *shader_path, glslang_output *output) if (!glslang_read_shader_file(shader_path, &lines, true, false)) goto error; + + /* Compute cache key from preprocessed source (vertex + fragment stages) */ + { + std::string vertex_source = build_stage_source(&lines, "vertex"); + std::string fragment_source = build_stage_source(&lines, "fragment"); + spirv_cache_compute_hash(vertex_source.c_str(), fragment_source.c_str(), + cache_filename); + } + + /* Try to load from cache */ + if (spirv_cache_load(cache_filename, output)) + { + RARCH_LOG("[Slang] Loaded shader from cache: \"%s\".\n", shader_path); + shader_line_buf_free(&lines); + return true; + } + output->meta = glslang_meta{}; if (!glslang_parse_meta(&lines, &output->meta)) goto error; @@ -759,6 +778,9 @@ bool glslang_compile_shader(const char *shader_path, glslang_output *output) goto error; } + /* Save to cache */ + spirv_cache_save(cache_filename, output); + shader_line_buf_free(&lines); return true; diff --git a/griffin/griffin_cpp.cpp b/griffin/griffin_cpp.cpp index f7f882e7ffc4..f9e1e27816fb 100644 --- a/griffin/griffin_cpp.cpp +++ b/griffin/griffin_cpp.cpp @@ -79,6 +79,7 @@ VIDEO DRIVER #include "../deps/SPIRV-Cross/spirv_cross_parsed_ir.cpp" #ifdef HAVE_SLANG #include "../gfx/drivers_shader/slang_process.cpp" +#include "../gfx/drivers_shader/slang_cache.cpp" #endif #endif