Browse Source

Add shader_template class and revise the shader program loader

master
C. J. Howard 2 years ago
parent
commit
f35a6fa7c7
4 changed files with 409 additions and 174 deletions
  1. +0
    -1
      CMakeLists.txt
  2. +222
    -0
      src/gl/shader-template.cpp
  3. +136
    -0
      src/gl/shader-template.hpp
  4. +51
    -173
      src/resources/shader-program-loader.cpp

+ 0
- 1
CMakeLists.txt View File

@ -14,7 +14,6 @@ find_package(SDL2 REQUIRED COMPONENTS SDL2::SDL2-static SDL2::SDL2main CONFIG)
find_package(OpenAL REQUIRED CONFIG)
find_library(physfs REQUIRED NAMES physfs-static PATHS "${CMAKE_PREFIX_PATH}/lib")
# Determine dependencies
set(STATIC_LIBS
dr_wav

+ 222
- 0
src/gl/shader-template.cpp View File

@ -0,0 +1,222 @@
/*
* Copyright (C) 2021 Christopher J. Howard
*
* This file is part of Antkeeper source code.
*
* Antkeeper source code is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Antkeeper source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Antkeeper source code. If not, see <http://www.gnu.org/licenses/>.
*/
#include "gl/shader-template.hpp"
#include <algorithm>
#include <sstream>
namespace gl {
shader_template::shader_template(const std::string& source_code)
{
source(source_code);
}
shader_template::shader_template()
{}
void shader_template::source(const std::string& source)
{
// Reset template
template_source.clear();
vertex_directives.clear();
fragment_directives.clear();
geometry_directives.clear();
define_directives.clear();
// Iterate through source line-by-line
std::istringstream source_stream(source);
std::string line;
while (std::getline(source_stream, line))
{
std::string token;
std::istringstream line_stream(line);
// Detect `#pragma` directives
if (line_stream >> token && token == "#pragma")
{
if (line_stream >> token)
{
// Map line numbers of supported directives
if (token == "define")
{
if (line_stream >> token)
define_directives.insert({token, template_source.size()});
}
else if (token == "vertex")
vertex_directives.insert(template_source.size());
else if (token == "fragment")
fragment_directives.insert(template_source.size());
else if (token == "geometry")
geometry_directives.insert(template_source.size());
}
}
// Append line to template source
template_source.push_back(line);
}
}
std::string shader_template::configure(gl::shader_stage stage, const dictionary_type& definitions) const
{
replace_stage_directives(stage);
replace_define_directives(definitions);
// Join vector of source lines into single string
std::ostringstream stream;
std::copy(template_source.begin(), template_source.end(), std::ostream_iterator<std::string>(stream, "\n"));
return stream.str();
}
gl::shader_object* shader_template::compile(gl::shader_stage stage, const dictionary_type& definitions) const
{
// Generate shader object source
std::string object_source = configure(stage, definitions);
// Create new shader object
gl::shader_object* object = new gl::shader_object(stage);
// Set shader object source
object->source(object_source.c_str(), object_source.length());
// Compile shader object
object->compile();
return object;
}
gl::shader_program* shader_template::build(const dictionary_type& definitions) const
{
gl::shader_object* vertex_object = nullptr;
gl::shader_object* fragment_object = nullptr;
gl::shader_object* geometry_object = nullptr;
// Create shader program
gl::shader_program* program = new gl::shader_program();
if (has_vertex_directive())
{
// Compile vertex shader object and attach to shader program
vertex_object = compile(gl::shader_stage::vertex, definitions);
program->attach(vertex_object);
}
if (has_fragment_directive())
{
// Compile fragment shader object and attach to shader program
fragment_object = compile(gl::shader_stage::fragment, definitions);
program->attach(fragment_object);
}
if (has_geometry_directive())
{
// Compile fragment shader object and attach to shader program
geometry_object = compile(gl::shader_stage::geometry, definitions);
program->attach(geometry_object);
}
// Link attached shader objects into shader program
program->link();
if (vertex_object)
{
// Detach and delete vertex shader object
program->detach(vertex_object);
delete vertex_object;
}
if (fragment_object)
{
// Detach and delete fragment shader object
program->detach(fragment_object);
delete fragment_object;
}
if (geometry_object)
{
// Detach and delete geometry shader object
program->detach(geometry_object);
delete geometry_object;
}
return program;
}
void shader_template::replace_stage_directives(gl::shader_stage stage) const
{
// Determine stage directives according to the shader stage being generated
const std::string vertex_directive = (stage == gl::shader_stage::vertex) ? "#define __VERTEX__" : "/* #undef __VERTEX__ */";
const std::string fragment_directive = (stage == gl::shader_stage::fragment) ? "#define __FRAGMENT__" : "/* #undef __FRAGMENT__ */";
const std::string geometry_directive = (stage == gl::shader_stage::geometry) ? "#define __GEOMETRY__" : "/* #undef __GEOMETRY__ */";
// Handle `#pragma <stage>` directives
for (std::size_t i: vertex_directives)
template_source[i] = vertex_directive;
for (std::size_t i: fragment_directives)
template_source[i] = fragment_directive;
for (std::size_t i: geometry_directives)
template_source[i] = geometry_directive;
}
void shader_template::replace_define_directives(const dictionary_type& definitions) const
{
// For each `#pragma define <key>` directive
for (const auto& define_directive: define_directives)
{
// Get a reference to the directive line
std::string& line = template_source[define_directive.second];
// Check if the corresponding definition was given by the configuration
auto definitions_it = definitions.find(define_directive.first);
if (definitions_it != definitions.end())
{
// Definition found, Replace `#pragma define <key>` with `#define <key>` or `#define <key> <value>`
line = "#define " + define_directive.first;
if (!definitions_it->second.empty())
line += " " + definitions_it->second;
}
else
{
// Definition not found, replace `#pragma define <key>` with the comment `/* #undef <key> */`.
line = "/* #undef " + define_directive.first + " */";
}
}
}
bool shader_template::has_vertex_directive() const
{
return !vertex_directives.empty();
}
bool shader_template::has_fragment_directive() const
{
return !fragment_directives.empty();
}
bool shader_template::has_geometry_directive() const
{
return !geometry_directives.empty();
}
bool shader_template::has_define_directive(const std::string& name) const
{
return (define_directives.find(name) != define_directives.end());
}
} // namespace gl

+ 136
- 0
src/gl/shader-template.hpp View File

@ -0,0 +1,136 @@
/*
* Copyright (C) 2021 Christopher J. Howard
*
* This file is part of Antkeeper source code.
*
* Antkeeper source code is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Antkeeper source code is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Antkeeper source code. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ANTKEEPER_SHADER_TEMPLATE_HPP
#define ANTKEEPER_SHADER_TEMPLATE_HPP
#include "gl/shader-object.hpp"
#include "gl/shader-program.hpp"
#include <map>
#include <string>
#include <unordered_set>
#include <vector>
namespace gl {
/**
* Shader templates can be used to generate multiple shader variants from a single source.
*
* Shader templates support the following preprocessor directives:
*
* * `#pragma vertex`: Replaced with `#define __VERTEX__` when generating vertex shader objects.
* * `#pragma fragment`: Replaced with `#define __FRAGMENT__` when generating fragment shader objects.
* * `#pragma geometry`: Replaced with `#define __GEOMETRY__` when generating geometry shader objects.
* * `#pragma define <name> <value>`: Will be replaced with `#define <name> <value>` if its definition is passed to the shader template.
*
* @see gl::shader_stage
* @see gl::shader_object
* @see gl::shader_program
*/
class shader_template
{
public:
/// Container of definitions used to replace `#pragma define <key> <value>` directives.
typedef std::unordered_map<std::string, std::string> dictionary_type;
/**
* Creates a shader template and sets its source code.
*
* @param source_code String containing the shader template source code.
*
* @see shader_template::source(const std::string&)
*/
shader_template(const std::string& source_code);
/**
* Creates a shader template.
*/
shader_template();
/**
* Replaces the source code of the shader template.
*
* @param source_code String containing the shader template source code.
*/
void source(const std::string& source_code);
/**
* Configures shader object source code given a shader stage and template dictionary.
*
* @param stage Shader stage of the shader object to generate. Instances of `#pragma <stage>` in the template source will be replaced with `#define __<STAGE>__`.
* @param definitions Container of definitions used to replace `#pragma define <key> <value>` directives.
* @return Configured shader object source code.
*/
std::string configure(gl::shader_stage stage, const dictionary_type& definitions) const;
/**
* Configures and compiles a shader object.
*
* @param stage Shader stage of the shader object to generate. Instances of `#pragma <stage>` in the template source will be replaced with `#define __<STAGE>__`.
* @param definitions Container of definitions used to replace `#pragma define <key> <value>` directives.
* @return Compiled shader object.
*
* @exception std::runtime_error Any exceptions thrown by gl::shader_object.
*/
gl::shader_object* compile(gl::shader_stage stage, const dictionary_type& definitions) const;
/**
* Configures and compiles shader objects, then links them into a shader program. Shader object stages are determined according to the presence of `#pragma <stage>` directives.
*
* @param definitions Container of definitions used to replace `#pragma define <key> <value>` directives.
* @return Linked shader program.
*
* @exception std::runtime_error Any exceptions thrown by gl::shader_object or gl::shader_program.
*
* @see has_vertex_directive() const
* @see has_fragment_directive() const
* @see has_geometry_directive() const
*/
gl::shader_program* build(const dictionary_type& definitions) const;
/// Returns `true` if the template source contains one or more `#pragma vertex` directive.
bool has_vertex_directive() const;
/// Returns `true` if the template source contains one or more `#pragma fragment` directive.
bool has_fragment_directive() const;
/// Returns `true` if the template source contains one or more `#pragma geometry` directive.
bool has_geometry_directive() const;
/**
* Returns `true` if the template source contains one or more instance of `#pragma define <name>`.
*
* @param name Definition name.
*/
bool has_define_directive(const std::string& name) const;
private:
void replace_stage_directives(gl::shader_stage stage) const;
void replace_define_directives(const dictionary_type& definitions) const;
mutable std::vector<std::string> template_source;
std::unordered_set<std::size_t> vertex_directives;
std::unordered_set<std::size_t> fragment_directives;
std::unordered_set<std::size_t> geometry_directives;
std::multimap<std::string, std::size_t> define_directives;
};
} // namespace gl
#endif // ANTKEEPER_SHADER_TEMPLATE_HPP

+ 51
- 173
src/resources/shader-program-loader.cpp View File

@ -20,26 +20,8 @@
#include "resources/resource-loader.hpp"
#include "resources/resource-manager.hpp"
#include "resources/text-file.hpp"
#include "gl/shader-stage.hpp"
#include "gl/shader-object.hpp"
#include "gl/shader-program.hpp"
#include "gl/shader-template.hpp"
#include <sstream>
#include <unordered_set>
#include <iterator>
/**
* Tokenizes a line of shader source code.
*/
static std::vector<std::string> tokenize(const std::string& line)
{
std::vector<std::string> tokens;
std::string token;
std::istringstream linestream(line);
while (linestream >> token)
tokens.push_back(token);
return tokens;
}
/**
* Handles `#pragma include` directives by loading the specified text files and inserting them in place.
@ -49,179 +31,75 @@ static void handle_includes(text_file* source, resource_manager* resource_manage
// For each line in the source
for (std::size_t i = 0; i < source->size(); ++i)
{
// Tokenize line
std::vector<std::string> tokens = tokenize((*source)[i]);
// Look for `#pragma include` directives
if (tokens.size() == 3 && tokens[0] == "#pragma" && tokens[1] == "include")
{
// Get path to include file
std::string path = tokens[2].substr(1, tokens[2].length() - 2);
// Load include file
if (!resource_manager->load<text_file>(path))
{
std::string message = std::string("Failed to load shader include file \"") + path + std::string("\"");
throw std::runtime_error(message.c_str());
}
text_file include_file = *(resource_manager->load<text_file>(path));
// Handle `#pragma include` directives inside include file
handle_includes(&include_file, resource_manager);
// Replace #pragma include directive with include file contents
source->erase(source->begin() + i);
source->insert(source->begin() + i, include_file.begin(), include_file.end());
i += include_file.size() - 1;
}
}
}
/**
* Injects the `DEBUG` or `NDEBUG` macros after the `#version` directive.
*/
static void inject_debug_macro(text_file* source)
{
// For each line in the source
for (std::size_t i = 0; i < source->size(); ++i)
{
// Tokenize line
std::vector<std::string> tokens = tokenize((*source)[i]);
// Inject DEBUG and NDEBUG macros
if (!tokens.empty() && tokens[0] == "#version")
{
#if defined(NDEBUG)
source->insert(source->begin() + i + 1, "#define NDEBUG");
#else
source->insert(source->begin() + i + 1, "#define DEBUG");
#endif
return;
}
}
}
/**
* Injects a shader type macro definition after the `#version` directive.
*/
static void inject_shader_stage_macro(text_file* source, gl::shader_stage stage)
{
const char* vertex_macro = "#define _VERTEX";
const char* fragment_macro = "#define _FRAGMENT";
const char* geometry_macro = "#define _GEOMETRY";
const char* macro = nullptr;
if (stage == gl::shader_stage::vertex)
macro = vertex_macro;
else if (stage == gl::shader_stage::fragment)
macro = fragment_macro;
else if (stage == gl::shader_stage::geometry)
macro = geometry_macro;
// For each line in the source
for (std::size_t i = 0; i < source->size(); ++i)
{
// Tokenize line
std::vector<std::string> tokens = tokenize((*source)[i]);
// Inject shader stage macro
if (!tokens.empty() && tokens[0] == "#version")
{
source->insert(source->begin() + i + 1, macro);
return;
}
}
}
static std::unordered_set<gl::shader_stage> detect_shader_stages(text_file* source)
{
std::unordered_set<gl::shader_stage> types;
// For each line in the source
for (std::size_t i = 0; i < source->size(); ++i)
{
// Tokenize line
std::vector<std::string> tokens = tokenize((*source)[i]);
// Look for `#pragma include` directives
if (tokens.size() == 2 && tokens[0] == "#pragma")
std::string token;
std::istringstream line_stream((*source)[i]);
// If line contains a `#pragma include` directive
if (line_stream >> token && token == "#pragma" &&
line_stream >> token && token == "include")
{
if (tokens[1] == "vertex")
// If third token is enclosed in quotes or angled brackets
if (line_stream >> token && token.size() > 2 &&
((token.front() == '\"' && token.back() == '\"') ||
(token.front() == '<' && token.back() == '>')))
{
types.insert(gl::shader_stage::vertex);
// Extract include path
std::string path = token.substr(1, token.length() - 2);
// Load include file
if (!resource_manager->load<text_file>(path))
{
(*source)[i] = "#error file not found (" + path + ")";
}
else
{
// Create copy of include file
text_file include_file = *(resource_manager->load<text_file>(path));
// Handle `#pragma include` directives inside include file
handle_includes(&include_file, resource_manager);
// Replace #pragma include directive with include file contents
source->erase(source->begin() + i);
source->insert(source->begin() + i, include_file.begin(), include_file.end());
i += include_file.size() - 1;
}
}
else if (tokens[1] == "fragment")
else
{
types.insert(gl::shader_stage::fragment);
}
else if (tokens[1] == "geometry")
{
types.insert(gl::shader_stage::geometry);
(*source)[i] = "#error malformed include directive";
}
}
}
return types;
}
static std::string generate_source_buffer(const std::vector<std::string>& source)
{
std::ostringstream stream;
std::copy(source.begin(), source.end(), std::ostream_iterator<std::string>(stream, "\n"));
return stream.str();
}
template <>
gl::shader_program* resource_loader<gl::shader_program>::load(resource_manager* resource_manager, PHYSFS_File* file)
{
// Load shader source
text_file* source = resource_loader<text_file>::load(resource_manager, file);
// Load shader template source
text_file source_lines = *resource_loader<text_file>::load(resource_manager, file);
// Handle `#pragma include` directives
handle_includes(source, resource_manager);
// Inject `DEBUG` or `NDEBUG` macro definitions
inject_debug_macro(source);
// Detect declared shader types via the `#pragma vertex`, `#pragma fragment` and `#pragma geometry` directives
std::unordered_set<gl::shader_stage> shade_stages = detect_shader_stages(source);
handle_includes(&source_lines, resource_manager);
// Load detected shaders
std::list<gl::shader_object*> objects;
for (gl::shader_stage stage: shade_stages)
{
text_file stage_source = *source;
inject_shader_stage_macro(&stage_source, stage);
std::string source_buffer = generate_source_buffer(stage_source);
gl::shader_object* object = new gl::shader_object(stage);
object->source(source_buffer.c_str(), source_buffer.length());
object->compile();
objects.push_back(object);
}
// Create shader program
gl::shader_program* program = new gl::shader_program();
// Join vector of source lines into single string
std::ostringstream stream;
std::copy(source_lines.begin(), source_lines.end(), std::ostream_iterator<std::string>(stream, "\n"));
// Attach shader objects
for (gl::shader_object* object: objects)
program->attach(object);
// Create shader template
gl::shader_template* shader_template = new gl::shader_template(stream.str());
// Link shader program
if (!program->link())
// Build shader program
gl::shader_program* program = shader_template->build(gl::shader_template::dictionary_type());
// Check if shader program was linked successfully
if (!program->was_linked())
{
throw std::runtime_error("Shader program linking failed: " + program->get_info_log());
}
// Detach and delete shader objects
for (gl::shader_object* object: objects)
{
program->detach(object);
delete object;
}
// Destroy shader template
delete shader_template;
return program;
}

Loading…
Cancel
Save