linux-wallpaperengine/src/WallpaperEngine/Render/Shaders/Compiler.cpp
Alexis Maiquez 5521e90155 Ignore composelayers for now
Added support for color blend mode
Main pass' blendmode should be normal
Last pass' blendmode should be the first's
Fixed order of glClear's so _rt_FullFrameBuffer is the right color at the right time
Simplified effect and pass creation code a bit more
Non-visible images should render the full chain into the pingpong framebuffers instead of to screen
Fixed an integer division not producing float numbers

Signed-off-by: Alexis Maiquez <almamu@almamu.com>
2022-11-03 13:14:52 +01:00

778 lines
28 KiB
C++

#include <iostream>
#include <fstream>
#include <string>
#include <utility>
// filesystem
#include <WallpaperEngine/FileSystem/FileSystem.h>
// shader compiler
#include <WallpaperEngine/Render/Shaders/Compiler.h>
#include <WallpaperEngine/Core/Objects/Effects/Constants/CShaderConstantVector4.h>
#include <WallpaperEngine/Core/Objects/Effects/Constants/CShaderConstantInteger.h>
#include <WallpaperEngine/Core/Objects/Effects/Constants/CShaderConstantFloat.h>
#include "WallpaperEngine/Assets/CAssetLoadException.h"
#include "WallpaperEngine/Render/Shaders/Variables/CShaderVariable.h"
#include "WallpaperEngine/Render/Shaders/Variables/CShaderVariableFloat.h"
#include "WallpaperEngine/Render/Shaders/Variables/CShaderVariableInteger.h"
#include "WallpaperEngine/Render/Shaders/Variables/CShaderVariableVector2.h"
#include "WallpaperEngine/Render/Shaders/Variables/CShaderVariableVector3.h"
#include "WallpaperEngine/Render/Shaders/Variables/CShaderVariableVector4.h"
using namespace WallpaperEngine::Core;
using namespace WallpaperEngine::Assets;
namespace WallpaperEngine::Render::Shaders
{
Compiler::Compiler (
CContainer* container,
std::string filename,
Type type,
std::map <std::string, int>* combos,
std::map <std::string, bool>* foundCombos,
const std::vector <std::string>& textures,
const std::map<std::string, CShaderConstant*>& constants,
bool recursive) :
m_combos (combos),
m_foundCombos (foundCombos),
m_passTextures (textures),
m_recursive (recursive),
m_type (type),
m_file (std::move(filename)),
m_error (""),
m_errorInfo (""),
m_constants (constants),
m_container (container),
m_baseCombos ()
{
if (type == Type_Vertex)
this->m_content = this->m_container->readVertexShader (this->m_file);
else if (type == Type_Pixel)
this->m_content = this->m_container->readFragmentShader (this->m_file);
else if (type == Type_Include)
this->m_content = this->m_container->readIncludeShader (this->m_file);
// clone the combos into the baseCombos to keep track of values that must be embedded no matter what
auto cur = this->m_combos->begin ();
auto end = this->m_combos->end ();
for (; cur != end; cur ++)
this->m_baseCombos.insert (std::make_pair ((*cur).first, (*cur).second));
}
bool Compiler::peekString(std::string str, std::string::const_iterator& it)
{
std::string::const_iterator check = str.begin();
std::string::const_iterator cur = it;
while (cur != this->m_content.end () && check != str.end ())
{
if (*cur != *check) return false;
cur ++; check ++;
}
if (cur == this->m_content.end ())
{
return false;
}
if (check != str.end ())
{
return false;
}
it = cur;
return true;
}
bool Compiler::expectSemicolon (std::string::const_iterator& it)
{
if (*it != ';')
{
this->m_error = true;
this->m_errorInfo = "Expected semicolon but got " + *it;
return false;
}
it ++;
return true;
}
void Compiler::ignoreSpaces(std::string::const_iterator &it)
{
while (it != this->m_content.end() && (*it == ' ' || *it == '\t')) it ++;
}
void Compiler::ignoreUpToNextLineFeed (std::string::const_iterator& it)
{
while (it != this->m_content.end() && *it != '\n') it ++;
}
void Compiler::ignoreUpToBlockCommentEnd (std::string::const_iterator& it)
{
while (it != this->m_content.end() && this->peekString ("*/", it) == false) it ++;
}
std::string Compiler::extractType (std::string::const_iterator& it)
{
// first of all check for highp/mediump/lowp as these operators have to be ignored
this->peekString ("highp", it);
this->peekString ("mediump", it);
this->peekString ("lowp", it);
this->ignoreSpaces (it);
auto cur = sTypes.begin ();
auto end = sTypes.end ();
while (cur != end)
{
if (this->peekString (*cur + " ", it) == true)
{
return *cur;
}
cur ++;
}
this->m_error = true;
this->m_errorInfo = "Expected type";
return "";
}
std::string Compiler::extractName (std::string::const_iterator& it)
{
std::string::const_iterator cur = it;
std::string::const_iterator begin = cur;
// first character has to be a valid alphabetic characer
if (this->isChar (cur) == false && *cur != '_')
{
this->m_error = true;
this->m_errorInfo = "Expected name doesn't start with a valid character";
return "";
}
cur ++;
while (cur != this->m_content.end () && (this->isChar (cur) == true || *cur == '_' || this->isNumeric (cur) == true)) cur ++;
it = cur;
return std::string (begin, cur);
}
std::string Compiler::extractArray(std::string::const_iterator &it, bool mustExists)
{
std::string::const_iterator cur = it;
std::string::const_iterator begin = cur;
if (*cur != '[')
{
if (mustExists == false)
return "";
this->m_error = true;
this->m_errorInfo = "Expected an array but found nothing";
return "";
}
cur ++;
while (cur != this->m_content.end () && *cur != ']') cur ++;
it = ++cur;
return std::string (begin, cur);
}
bool Compiler::isChar (std::string::const_iterator& it)
{
return ((*it) >= 'A' && (*it) <= 'Z') || ((*it) >= 'a' && (*it) <= 'z');
}
bool Compiler::isNumeric (std::string::const_iterator& it)
{
return (*it) >= '0' && (*it) <= '9';
}
std::string Compiler::extractQuotedValue(std::string::const_iterator& it)
{
std::string::const_iterator cur = it;
if (*cur != '"')
{
m_error = true;
m_errorInfo = "Expected opening \" but got " + (*cur);
return "";
}
cur ++;
while (cur != this->m_content.end () && *cur != '\n' && *cur != '"') cur ++;
if (cur == this->m_content.end ())
{
m_error = true;
m_errorInfo = "Expected closing \" not found";
it = cur;
return "";
}
std::string filename = std::string (++it, cur);
it = ++cur;
return filename;
}
std::string Compiler::lookupShaderFile (std::string filename)
{
// now compile the new shader
// do not include the default header (as it's already included in the parent)
Compiler loader (this->m_container, std::move (filename), Type_Include, this->m_combos, this->m_foundCombos, this->m_passTextures, this->m_constants, true);
loader.precompile ();
return loader.getCompiled ();
}
std::string& Compiler::getCompiled ()
{
return this->m_compiledContent;
}
void Compiler::precompile()
{
// TODO: SEPARATE THIS IN TWO SO THE COMBOS ARE DETECTED FIRST AND WE DO NOT REQUIRE DOUBLE COMPILATION OF THE SHADER'S SOURCE
#define BREAK_IF_ERROR if (this->m_error == true) { throw std::runtime_error ("ERROR PRE-COMPILING SHADER" + this->m_errorInfo); }
// parse the shader and find #includes and such things and translate them to the correct name
// also remove any #version definition to prevent errors
std::string::const_iterator it = this->m_content.begin ();
// reset error indicator
this->m_error = false;
this->m_errorInfo = "";
this->m_compiledContent = "";
this->m_includesContent = "";
this->m_includesProcessed = false;
// search preprocessor macros and parse them
while (it != this->m_content.end () && this->m_error == false)
{
if (*it == ' ' || *it == '\t' || *it == '\n' || *it == '\r' || *it == '\0' || *it == '{' || *it == '}' || *it == '[' || *it == ']' || *it == '.')
{
this->m_compiledContent += *it;
it ++;
}
else if (*it == '#')
{
if (this->peekString ("#include ", it) == true)
{
std::string filename = "";
// ignore whitespaces
this->ignoreSpaces (it); BREAK_IF_ERROR
// extract value between quotes
filename = this->extractQuotedValue (it); BREAK_IF_ERROR
if (this->m_recursive)
{
this->m_compiledContent += "// begin of include from file " + filename + "\r\n";
this->m_compiledContent += this->lookupShaderFile (filename);
this->m_compiledContent += "\r\n// end of included from file " + filename + "\r\n";
}
else
{
// load the content to the includes contents and continue with the next one
// try to find the file first
this->m_includesContent += "// begin of included from file " + filename + "\r\n";
this->m_includesContent += this->lookupShaderFile (filename);
this->m_includesContent += "\r\n// end of included from file " + filename + "\r\n";
}
}
else
{
this->m_compiledContent += '#';
it ++;
}
}
else if (*it == 'u')
{
// uniforms might have extra information for their values
if (this->peekString ("uniform ", it) == true)
{
this->ignoreSpaces (it);
std::string type = this->extractType (it); BREAK_IF_ERROR
this->ignoreSpaces (it);
std::string name = this->extractName (it); BREAK_IF_ERROR
this->ignoreSpaces (it);
std::string array = this->extractArray (it, false); BREAK_IF_ERROR
this->ignoreSpaces (it);
this->expectSemicolon (it); BREAK_IF_ERROR
this->ignoreSpaces (it);
// check if there is any actual extra information and parse it
if (this->peekString ("//", it) == true)
{
this->ignoreSpaces (it);
std::string::const_iterator begin = it;
this->ignoreUpToNextLineFeed (it);
std::string configuration; configuration.append (begin, it);
// parse the parameter information
this->parseParameterConfiguration (type, name, configuration); BREAK_IF_ERROR
this->m_compiledContent += "uniform " + type + " " + name + array + "; // " + configuration;
}
else
{
this->m_compiledContent += "uniform " + type + " " + name + array + ";";
}
}
else
{
// continue reading the original shader as this most likely is a variable name or something
// that is not really interesting for the compiler
this->m_compiledContent += *it;
it ++;
}
}
else if (*it == 'a')
{
// find attribute definitions
if (this->peekString ("attribute ", it) == true)
{
this->ignoreSpaces (it);
std::string type = this->extractType (it); BREAK_IF_ERROR
this->ignoreSpaces (it);
std::string name = this->extractName (it); BREAK_IF_ERROR
this->ignoreSpaces (it);
std::string array = this->extractArray (it, false); BREAK_IF_ERROR
this->ignoreSpaces (it);
this->expectSemicolon (it); BREAK_IF_ERROR
this->m_compiledContent += "attribute " + type + " " + name + array + ";";
}
else
{
// check for types first
std::string type = this->extractType (it);
// types not found, try names
if (this->m_error == false)
{
this->m_compiledContent += type;
}
else
{
this->m_error = false;
std::string name = this->extractName (it);
if (this->m_error == false)
{
// check if the name is a translated one or not
this->m_compiledContent += name;
}
else
{
this->m_error = false;
this->m_compiledContent += *it;
it ++;
}
}
}
}
else if (*it == '/')
{
if (this->peekString ("//", it) == true)
{
std::string::const_iterator begin = it - 2;
// is there a COMBO mark to take care of?
this->ignoreSpaces (it);
if (this->peekString ("[COMBO]", it) == true)
{
// parse combo json data to define the proper variables
this->ignoreSpaces (it);
begin = it;
this->ignoreUpToNextLineFeed (it);
std::string configuration; configuration.append (begin, it);
this->m_compiledContent += "// [COMBO] " + configuration;
this->parseComboConfiguration (configuration, 0); BREAK_IF_ERROR;
}
else if (this->peekString ("[COMBO_OFF]", it) == true)
{
// parse combo json data to define the proper variables
this->ignoreSpaces (it);
begin = it;
this->ignoreUpToNextLineFeed (it);
std::string configuration; configuration.append (begin, it);
this->m_compiledContent += "// [COMBO_OFF] " + configuration;
this->parseComboConfiguration (configuration, 0); BREAK_IF_ERROR;
}
else
{
// the comment can be ignored and put back as is
this->ignoreUpToNextLineFeed(it);
this->m_compiledContent.append (begin, it);
}
}
else if (this->peekString ("/*", it) == true)
{
std::string::const_iterator begin = it - 2;
this->ignoreUpToBlockCommentEnd (it);
this->m_compiledContent.append (begin, it);
}
else
{
this->m_compiledContent += *it;
it ++;
}
}
else
{
// check for types first
std::string type = this->extractType (it);
// types not found, try names
if (this->m_error == false)
{
// check for main, and take it into account, this also helps adding the includes
std::string name = this->extractName (it);
this->ignoreSpaces (it);
if (this->peekString ("(", it) == true)
{
if (this->m_includesProcessed == false)
{
this->m_compiledContent += "\n\n" + this->m_includesContent + "\n\n";
this->m_includesProcessed = true;
}
this->m_compiledContent += type + " " + name + "(";
}
else
{
this->m_compiledContent += type + " " + name;
}
}
else
{
this->m_error = false;
std::string name = this->extractName (it);
if (this->m_error == false)
{
// check if the name is a translated one or not
this->m_compiledContent += name;
}
else
{
this->m_error = false;
this->m_compiledContent += *it++;
}
}
}
}
std::string finalCode;
if (this->m_recursive == false)
{
// add the opengl compatibility at the top
finalCode = "#version 130\n"
"// ======================================================\n"
"// Processed shader " + this->m_file + "\n"
"// ======================================================\n"
"#define highp\n"
"#define mediump\n"
"#define lowp\n"
"#define mul(x, y) ((y) * (x))\n"
"#define max(x, y) max (y, x)\n"
"#define fmod(x, y) (x-y*trunc(x/y))\n"
"#define lerp mix\n"
"#define frac fract\n"
"#define CAST2(x) (vec2(x))\n"
"#define CAST3(x) (vec3(x))\n"
"#define CAST4(x) (vec4(x))\n"
"#define CAST3X3(x) (mat3(x))\n"
"#define saturate(x) (clamp(x, 0.0, 1.0))\n"
"#define texSample2D texture\n"
"#define texSample2DLod textureLod\n"
"#define atan2 atan\n"
"#define ddx dFdx\n"
"#define ddy(x) dFdy(-(x))\n"
"#define GLSL 1\n"
"#define float1 float\n"
"#define float2 vec2\n"
"#define float3 vec3\n"
"#define float4 vec4\n\n";
finalCode += "// ======================================================\n"
"// Shader combo parameter definitions\n"
"// ======================================================\n";
{
// add combo values
auto cur = this->m_foundCombos->begin ();
auto end = this->m_foundCombos->end ();
for (; cur != end; cur++)
{
// find the right value for the combo in the combos map
auto combo = this->m_combos->find ((*cur).first);
if (combo == this->m_combos->end ())
continue;
finalCode += "#define " + (*cur).first + " " + std::to_string ((*combo).second) + "\n";
}
}
// add base combos that come from the pass change that MUST be added
{
auto cur = this->m_baseCombos.begin ();
auto end = this->m_baseCombos.end ();
for (; cur != end; cur ++)
{
auto alreadyFound = this->m_foundCombos->find ((*cur).first);
if (alreadyFound != this->m_foundCombos->end ())
continue;
finalCode += "#define " + (*cur).first + " " + std::to_string ((*cur).second) + "\n";
}
}
}
finalCode += this->m_compiledContent;
if (DEBUG && this->m_recursive == false)
{
if (this->m_type == Type_Vertex)
std::cout << "======================== COMPILED VERTEX SHADER " << this->m_file.c_str () << " ========================" << std::endl;
else
std::cout << "======================== COMPILED FRAGMENT SHADER " << this->m_file.c_str () << " ========================" << std::endl;
std::cout << finalCode << std::endl;
}
// store the final final code here
this->m_compiledContent = finalCode;
#undef BREAK_IF_ERROR
}
void Compiler::parseComboConfiguration (const std::string& content, int defaultValue)
{
json data = json::parse (content);
auto combo = jsonFindRequired (data, "combo", "cannot parse combo information");
auto defvalue = data.find ("default");
// add line feed just in case
this->m_compiledContent += "\n";
// check the combos
std::map<std::string, int>::const_iterator entry = this->m_combos->find ((*combo).get <std::string> ());
// add the combo to the found list
this->m_foundCombos->insert (std::make_pair <std::string, int> (*combo, true));
// if the combo was not found in the predefined values this means that the default value in the JSON data can be used
// so only define the ones that are not already defined
if (entry == this->m_combos->end ())
{
// if no combo is defined just load the default settings
if (defvalue == data.end ())
{
// TODO: PROPERLY SUPPORT EMPTY COMBOS
this->m_combos->insert (std::make_pair <std::string, int> (*combo, (int) defaultValue));
}
else if ((*defvalue).is_number_float ())
{
throw std::runtime_error ("float combos not supported");
}
else if ((*defvalue).is_number_integer ())
{
this->m_combos->insert (std::make_pair <std::string, int> (*combo, (*defvalue).get <int> ()));
}
else if ((*defvalue).is_string ())
{
throw std::runtime_error ("string combos not supported");
}
else
{
throw std::runtime_error ("cannot parse combo information, unknown type");
}
}
}
void Compiler::parseParameterConfiguration (const std::string& type, const std::string& name, const std::string& content)
{
json data = json::parse (content);
auto material = data.find ("material");
auto defvalue = data.find ("default");
auto range = data.find ("range");
auto combo = data.find ("combo");
// this is not a real parameter
auto constant = this->m_constants.end ();
if (material != data.end ())
constant = this->m_constants.find (*material);
if (constant == this->m_constants.end () && defvalue == data.end ())
{
if (type != "sampler2D")
throw std::runtime_error ("cannot parse parameter data");
}
Variables::CShaderVariable* parameter = nullptr;
// TODO: SUPPORT VALUES FOR ALL THESE TYPES
if (type == "vec4")
{
parameter = new Variables::CShaderVariableVector4 (
constant == this->m_constants.end ()
? WallpaperEngine::Core::aToVector4 (*defvalue)
: *(*constant).second->as <CShaderConstantVector4> ()->getValue ()
);
}
else if (type == "vec3")
{
parameter = new Variables::CShaderVariableVector3 (
constant == this->m_constants.end ()
? WallpaperEngine::Core::aToVector3 (*defvalue)
: *(*constant).second->as <CShaderConstantVector4> ()->getValue ()
);
}
else if (type == "vec2")
{
parameter = new Variables::CShaderVariableVector2 (
WallpaperEngine::Core::aToVector2 (*defvalue)
);
}
else if (type == "float")
{
float value = 0;
if (constant == this->m_constants.end ())
value = (*defvalue).get <float> ();
else if ((*constant).second->is <CShaderConstantFloat> () == true)
value = *(*constant).second->as <CShaderConstantFloat> ()->getValue ();
else if ((*constant).second->is <CShaderConstantInteger> () == true)
value = *(*constant).second->as <CShaderConstantInteger> ()->getValue ();
parameter = new Variables::CShaderVariableFloat (value);
}
else if (type == "int")
{
int value = 0;
if (constant == this->m_constants.end ())
value = (*defvalue).get <int> ();
else if ((*constant).second->is <CShaderConstantFloat> () == true)
value = *(*constant).second->as <CShaderConstantFloat> ()->getValue ();
else if ((*constant).second->is <CShaderConstantInteger> () == true)
value = *(*constant).second->as <CShaderConstantInteger> ()->getValue ();
parameter = new Variables::CShaderVariableInteger (value);
}
else if (type == "sampler2D")
{
// samplers can have special requirements, check what sampler we're working with and create definitions
// if needed
auto combo = data.find ("combo");
auto textureName = data.find ("default");
// extract the texture number from the name
char value = name.at (std::string("g_Texture").length ());
// now convert it to integer
int index = value - '0';
if (combo != data.end ())
{
// if the texture exists (and is not null), add to the combo
if (this->m_passTextures.size () > index && (this->m_passTextures.at (index) != "" || textureName != data.end ()))
{
// add the new combo to the list
this->m_combos->insert (std::make_pair <std::string, int> (*combo, 1));
// textures linked to combos need to be tracked too
if (this->m_foundCombos->find (*combo) == this->m_foundCombos->end ())
this->m_foundCombos->insert (std::make_pair <std::string, bool> (*combo, true));
}
}
if (textureName != data.end ())
this->m_textures.insert (std::make_pair (index, *textureName));
// samplers are not saved, we can ignore them for now
return;
}
else
{
this->m_error = true;
this->m_errorInfo = "Unknown parameter type: " + type + " for " + name;
return;
}
if (material != data.end ())
{
parameter->setIdentifierName (*material);
parameter->setName (name);
this->m_parameters.push_back (parameter);
}
}
Variables::CShaderVariable* Compiler::findParameter (const std::string& identifier)
{
auto cur = this->m_parameters.begin ();
auto end = this->m_parameters.end ();
for (; cur != end; cur ++)
{
if ((*cur)->getIdentifierName () == identifier)
{
return (*cur);
}
}
return nullptr;
}
const std::vector <Variables::CShaderVariable*>& Compiler::getParameters () const
{
return this->m_parameters;
}
std::map <std::string, int>* Compiler::getCombos () const
{
return this->m_combos;
}
const std::map <int, std::string>& Compiler::getTextures () const
{
return this->m_textures;
}
std::vector<std::string> Compiler::sTypes =
{
"vec4", "uvec4", "ivec4", "dvec4", "bvec4",
"vec3", "uvec3", "ivec3", "dvec3", "bvec3",
"vec2", "uvec2", "ivec2", "dvec2", "bvec2",
"float", "sampler2D", "mat4x3", "mat4", "mat3", "uint4",
"void"
};
}