/** * @file shaders.hpp * @brief SPIR-V shader compilation and caching system. * * This file provides functionality for compiling GLSL shaders to SPIR-V and * managing a shader cache system. It supports automatic recompilation when * source files are modified and efficient caching of compiled shaders. */ #pragma once #include #include #include #include #include #include #include #define VULKAN_HPP_NO_CONSTRUCTORS #include #include "../core/types.hpp" namespace graphics { /** * @brief Handles shader compilation and caching operations. * * This class provides static methods for compiling GLSL shaders to SPIR-V * and managing a cache system. It automatically detects when shaders need * to be recompiled based on file timestamps and provides efficient caching * of compiled shader binaries. */ class ShaderCompiler { public: ShaderCompiler() = default; /** * @brief Compiles or retrieves a cached SPIR-V shader. * * @param shaderPath Path to the GLSL shader source file * @param kind Type of shader (vertex, fragment, compute, etc.) * @return std::vector Compiled SPIR-V binary code * @throws std::runtime_error If shader compilation fails or file is not found * * This function performs the following steps: * 1. Checks if a cached version exists and is up-to-date * 2. Loads from cache if available and valid * 3. Otherwise, compiles the shader from source * 4. Caches the newly compiled shader for future use * 5. Returns the SPIR-V binary code */ static fn getCompiledShader(const std::filesystem::path& shaderPath, const shaderc_shader_kind& kind) -> std::vector { using namespace std; // Convert to absolute path if relative filesystem::path absPath = filesystem::absolute(shaderPath); if (!filesystem::exists(absPath)) throw runtime_error("Shader file not found: " + absPath.string()); const string shaderName = absPath.stem().string(); const filesystem::path cacheFile = getCacheFilePath(shaderName); // Check if we need to recompile by comparing timestamps if (filesystem::exists(cacheFile)) { const auto sourceTime = filesystem::last_write_time(absPath); const auto cacheTime = filesystem::last_write_time(cacheFile); if (cacheTime >= sourceTime) { // Cache is up to date, load it vector spirvCode = loadCachedShader(cacheFile); if (!spirvCode.empty()) { fmt::println("Loaded shader from cache: {}", cacheFile.string()); return spirvCode; } } } // Need to compile the shader fmt::println("Compiling shader: {}", absPath.string()); // Read shader source ifstream file(absPath); if (!file) throw runtime_error("Failed to open shader file: " + absPath.string()); string shaderSource((istreambuf_iterator(file)), istreambuf_iterator()); file.close(); // Compile the shader vector spirvCode = compileShader(shaderSource.c_str(), kind); if (spirvCode.empty()) throw runtime_error("Shader compilation failed for: " + absPath.string()); // Cache the compiled SPIR-V binary saveCompiledShader(spirvCode, cacheFile.string()); return spirvCode; } /** * @brief Creates a shader module from SPIR-V code. * * @param device Logical device to create the shader module on * @param code SPIR-V binary code * @return vk::UniqueShaderModule Unique handle to the created shader module */ static fn createShaderModule(const vk::Device& device, const std::vector& code) -> vk::UniqueShaderModule { vk::ShaderModuleCreateInfo createInfo { .codeSize = code.size() * sizeof(u32), .pCode = code.data() }; return device.createShaderModuleUnique(createInfo); } private: /** * @brief Determines the platform-specific shader cache directory. * * @param shaderName Base name of the shader file * @return std::filesystem::path Full path to the cache file * * Cache locations: * - Windows: %LOCALAPPDATA%/VulkanApp/Shaders/ * - macOS: ~/Library/Application Support/VulkanApp/Shaders/ * - Linux: ~/.local/share/VulkanApp/Shaders/ * * The directory is created if it doesn't exist. */ static fn getCacheFilePath(const string& shaderName) -> std::filesystem::path { using namespace std::filesystem; #ifdef _WIN32 path cacheDir = path(getenv("LOCALAPPDATA")) / "VulkanApp" / "Shaders"; #elif defined(__APPLE__) path cacheDir = path(getenv("HOME")) / "Library" / "Application Support" / "VulkanApp" / "Shaders"; #else // Assume Linux or other UNIX-like systems path cacheDir = path(getenv("HOME")) / ".local" / "share" / "VulkanApp" / "Shaders"; #endif if (!exists(cacheDir)) create_directories(cacheDir); return cacheDir / (shaderName + ".spv"); } /** * @brief Loads a cached SPIR-V shader from disk. * * @param cachePath Path to the cached shader file * @return std::vector SPIR-V binary code, empty if loading fails * * Reads the binary SPIR-V data from the cache file. Returns an empty * vector if the file cannot be opened or read properly. */ static fn loadCachedShader(const std::filesystem::path& cachePath) -> std::vector { std::ifstream file(cachePath, std::ios::binary); if (!file.is_open()) return {}; // Read file size file.seekg(0, std::ios::end); const std::streamoff fileSize = file.tellg(); file.seekg(0, std::ios::beg); // Allocate buffer and read data std::vector buffer(static_cast(fileSize) / sizeof(u32)); file.read(std::bit_cast(buffer.data()), fileSize); return buffer; } /** * @brief Compiles GLSL source code to SPIR-V. * * @param source GLSL shader source code * @param kind Type of shader being compiled * @return std::vector Compiled SPIR-V binary code * * Uses the shaderc library to compile GLSL to SPIR-V. The compilation * is performed with optimization level set to performance and generates * debug information in debug builds. */ static fn compileShader(const char* source, shaderc_shader_kind kind) -> std::vector { shaderc::Compiler compiler; shaderc::CompileOptions options; // Set compilation options #ifdef NDEBUG options.SetOptimizationLevel(shaderc_optimization_level_performance); #else options.SetOptimizationLevel(shaderc_optimization_level_zero); options.SetGenerateDebugInfo(); #endif // Compile the shader shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, kind, "shader", options); if (module.GetCompilationStatus() != shaderc_compilation_status_success) return {}; return { module.cbegin(), module.cend() }; } /** * @brief Saves compiled SPIR-V code to the cache. * * @param spirv Compiled SPIR-V binary code * @param cachePath Path where the cache file should be saved * @return bool True if save was successful, false otherwise * * Writes the SPIR-V binary to disk for future use. Creates any * necessary parent directories if they don't exist. */ static fn saveCompiledShader(const std::vector& spirv, const std::string& cachePath) -> bool { std::ofstream file(cachePath, std::ios::binary); if (!file.is_open()) return false; file.write( std::bit_cast(spirv.data()), static_cast(spirv.size() * sizeof(u32)) ); return file.good(); } }; // class ShaderCompiler } // namespace graphics