230 lines
7.8 KiB
C++
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
|