vulkan-test/src/graphics/shaders.hpp
2024-11-15 17:18:08 -05:00

230 lines
7.8 KiB
C++

/**
* @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 <filesystem>
#include <fmt/format.h>
#include <fstream>
#include <ios>
#include <shaderc/shaderc.hpp>
#include <string>
#include <vector>
#define VULKAN_HPP_NO_CONSTRUCTORS
#include <vulkan/vulkan.hpp>
#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<u32> 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<u32> {
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<u32> 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<char>(file)), istreambuf_iterator<char>());
file.close();
// Compile the shader
vector<u32> 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<u32>& 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<u32> 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<u32> {
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<u32> buffer(static_cast<u32>(fileSize) / sizeof(u32));
file.read(std::bit_cast<char*>(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<u32> 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<u32> {
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<u32>& spirv, const std::string& cachePath) -> bool {
std::ofstream file(cachePath, std::ios::binary);
if (!file.is_open())
return false;
file.write(
std::bit_cast<const char*>(spirv.data()), static_cast<std::streamsize>(spirv.size() * sizeof(u32))
);
return file.good();
}
}; // class ShaderCompiler
} // namespace graphics