Skip to content

Instantly share code, notes, and snippets.

@yeaFern
Created August 7, 2020 15:32
Show Gist options
  • Save yeaFern/ecd60a9bb93cb75ef9c4f775cb1d9f3f to your computer and use it in GitHub Desktop.
Save yeaFern/ecd60a9bb93cb75ef9c4f775cb1d9f3f to your computer and use it in GitHub Desktop.
OpenGL Shader Program Caching
#include "graphics/Shader.h"
#include "graphics/ShaderCache.h"
namespace Kigu
{
Shader::Shader(const std::string name, const File& vertex, const File& fragment)
: m_Name(name), m_Program(glCreateProgram())
{
if (!ShaderCache::IsCachingSupported())
{
LogFatal("Shader caching is not available.");
}
// Check if the shader needs to be recompiled.
bool isCacheValid = true;
if (ShaderCache::NeedsCompilation(m_Name, vertex )) { isCacheValid = false; }
if (ShaderCache::NeedsCompilation(m_Name, fragment)) { isCacheValid = false; }
if (isCacheValid)
{
// If the cached binary is valid, load it.
bool success = LoadFromBinary();
// If the cached binary failed to link, recompile the shader.
if (!success)
{
LogWarn("Shader cache failed to link, recompiling.");
CompileAndSave(vertex, fragment);
}
}
else
{
// If the shader cache is invalid, recompile the shader.
LogInfo("Shader cache invalid, compiling.");
CompileAndSave(vertex, fragment);
}
}
GLint Shader::CompileShader(const File& source, ShaderType type)
{
if (!source.Exists())
{
LogFatal(source.GetPath() << " cannot be found.");
}
// Read the source file into a buffer.
GLint size = static_cast<GLint>(source.GetSize());
char* buffer = new char[size];
source.Read(buffer, size);
// Create the shader and pass it the source.
GLuint id = glCreateShader(static_cast<GLenum>(type));
glShaderSource(id, 1, &buffer, &size);
// Compile and check for errors.
glCompileShader(id);
GLint success;
glGetShaderiv(id, GL_COMPILE_STATUS, &success);
if (!success)
{
GLchar infoLog[1024];
glGetShaderInfoLog(id, 1024, nullptr, infoLog);
LogError("=====================================================");
LogError(infoLog);
LogError("== Caused by: " << source.GetFileName());
LogError("== Failed to compile Shader =========================");
}
// Attach the shader and clean up.
glAttachShader(m_Program, id);
delete[] buffer;
return id;
}
void Shader::CompileAndSave(const File& vertex, const File& fragment)
{
// Compile both shaders.
GLuint v = CompileShader(vertex, ShaderType::Vertex);
GLuint f = CompileShader(fragment, ShaderType::Fragment);
// Link and check for errors.
glLinkProgram(m_Program);
GLint success = 0;
GLchar errorLog[1024] = { 0 };
glGetProgramiv(m_Program, GL_LINK_STATUS, &success);
if (success == 0)
{
glGetProgramInfoLog(m_Program, sizeof(errorLog), NULL, errorLog);
LogError("=====================================================");
LogError(errorLog);
LogError("== Failed to link Shader ============================");
}
// Clean up the shaders.
glDeleteShader(v);
glDeleteShader(f);
// Get the size of the binary.
GLint binaryLength = 0;
glGetProgramiv(m_Program, GL_PROGRAM_BINARY_LENGTH, &binaryLength);
// Allocate space for the binary, as well as the binary format.
size_t bufferSize = sizeof(GLenum) + binaryLength;
char* binary = new char[sizeof(GLenum) + binaryLength];
// Get the binary from the driver, saving the format.
GLenum binaryFormat;
glGetProgramBinary(m_Program, binaryLength, nullptr, &binaryFormat, binary + sizeof(GLenum));
// Prefix the binary with the format.
*((GLenum*)binary) = binaryFormat;
// Write the binary to the cache.
ShaderCache::Write(m_Name, binary, bufferSize);
// Clean up.
delete[] binary;
}
bool Shader::LoadFromBinary()
{
// Get the binary file from the cache.
File binary = ShaderCache::Get(m_Name);
// Read it into a buffer.
char* buffer = new char[binary.GetSize()];
binary.Read(buffer, binary.GetSize());
// Grab the format from the front of the buffer;
GLenum format = *((GLenum*)buffer);
// Calculate offset to start of the shader binary.
void* binaryStart = buffer + sizeof(GLenum);
size_t binaryLength = binary.GetSize() - sizeof(GLenum);
// Upload the binary.
glProgramBinary(m_Program, format, binaryStart, binaryLength);
// Clean up.
delete[] buffer;
// Check for success.
GLint success = 0;
glGetProgramiv(m_Program, GL_LINK_STATUS, &success);
return success;
}
ShaderPtr Shader::Create(const std::string& name, const File& vertex, const File& fragment)
{
return std::make_unique<Shader>(name, vertex, fragment);
}
}
#pragma once
#include <memory>
#include <glad/glad.h>
#include "util/File.h"
#include "util/Log.h"
namespace Kigu
{
enum class ShaderType : GLenum
{
Vertex = GL_VERTEX_SHADER,
Fragment = GL_FRAGMENT_SHADER
};
class Shader
{
private:
std::string m_Name;
GLuint m_Program;
public:
Shader(
const std::string name,
const File& vertex,
const File& fragment
);
inline GLuint GetProgram() const { return m_Program; }
private:
GLint CompileShader(const File& source, ShaderType type);
void CompileAndSave(const File& vertex, const File& fragment);
bool LoadFromBinary();
public:
static std::unique_ptr<Shader> Create(const std::string& name, const File& vertex, const File& fragment);
};
using ShaderPtr = std::unique_ptr<Shader>;
}
#include "graphics/ShaderCache.h"
#include <iostream>
namespace Kigu
{
bool ShaderCache::IsCachingSupported()
{
// Shader caching is only available on GPUs which support at least one binary format.
GLint formats;
glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &formats);
return formats > 0;
}
bool ShaderCache::NeedsCompilation(const std::string& shaderName, const File& source)
{
// A shader needs compilation if either;
// - It does not exist in the shader cache.
// - The cached binary is older than the source file.
std::filesystem::path cache(ShaderCachePath);
if (!std::filesystem::exists(cache) || !std::filesystem::is_directory(cache))
{
// If the shader cache doesn't exist at all, create it.
std::filesystem::create_directories(cache);
// We can also guarantee that the cache is empty, and thus, the shader should be compiled.
return true;
}
File binary(ShaderCachePath + shaderName + ".bin");
if (!binary.Exists())
{
// If the binary does not exist, the shader needs to be compiled.
return true;
}
if (binary.GetLastWriteTime() < source.GetLastWriteTime())
{
// If the binary is out of date, the shader needs to be compiled.
return true;
}
return false;
}
File ShaderCache::Get(const std::string& shaderName)
{
std::filesystem::path cache(ShaderCachePath);
if (!std::filesystem::exists(cache) || !std::filesystem::is_directory(cache))
{
// If the shader cache doesn't exist at all, create it.
std::filesystem::create_directories(cache);
// Throw an error as the binary does not exist.
LogError("Shader binary does not exist for shader '" << shaderName << "'.");
}
File binary(ShaderCachePath + shaderName + ".bin");
if (!binary.Exists())
{
// Throw an error as the binary does not exist.
LogError("Shader binary does not exist for shader '" << shaderName << "'.");
}
return binary;
}
void ShaderCache::Write(const std::string& shaderName, char* buffer, size_t length)
{
std::filesystem::path cache(ShaderCachePath);
if (!std::filesystem::exists(cache) || !std::filesystem::is_directory(cache))
{
// If the shader cache doesn't exist at all, create it.
std::filesystem::create_directories(cache);
}
// Write the binary.
File binary(ShaderCachePath + shaderName + ".bin");
binary.Write(buffer, length);
}
}
#pragma once
#include <glad/glad.h>
#include "util/File.h"
#include "util/Log.h"
namespace Kigu
{
class ShaderCache
{
public:
static constexpr auto ShaderCachePath = "Data/ShaderCache/";
static bool IsCachingSupported();
static bool NeedsCompilation(const std::string& shaderName, const File& source);
static File Get(const std::string& shaderName);
static void Write(const std::string& shaderName, char* buffer, size_t length);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment