mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-08 22:07:20 +01:00
feat(vulkan): vulkan 1.4 core with the user interface and the launcher, shaders split into ogl46/vk14 with SPIR-V binaries loaded first
This commit is contained in:
689
rendering/common/shadersource.cpp
Normal file
689
rendering/common/shadersource.cpp
Normal file
@@ -0,0 +1,689 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "rendering/common/shadersource.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
|
||||
#include "utilities/Logs.h"
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool ends_with(std::string_view Value, std::string_view Ending)
|
||||
{
|
||||
return (Ending.size() <= Value.size()) && (Value.compare(Value.size() - Ending.size(), Ending.size(), Ending) == 0);
|
||||
}
|
||||
|
||||
// the content of a file, empty with Found false when it can't be read
|
||||
std::string read_text(std::string const &Path, bool &Found)
|
||||
{
|
||||
std::ifstream file(Path, std::ios::binary);
|
||||
Found = file.is_open();
|
||||
if (false == Found)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
std::stringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string read_file(std::string const &Path)
|
||||
{
|
||||
bool found{false};
|
||||
auto text{read_text(Path, found)};
|
||||
if ((false == found) || text.empty())
|
||||
{
|
||||
throw shader_exception("cannot read shader: " + Path);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, shader_components> const components_mapping = {
|
||||
{"R", shader_components::R}, {"RG", shader_components::RG}, {"RGB", shader_components::RGB}, {"RGBA", shader_components::RGBA}, {"sRGB", shader_components::sRGB}, {"sRGB_A", shader_components::sRGB_A}};
|
||||
|
||||
std::unordered_map<std::string, shader_param_default> const defaultparams_mapping = {
|
||||
{"required", shader_param_default::required}, {"nan", shader_param_default::nan}, {"zero", shader_param_default::zero}, {"one", shader_param_default::one},
|
||||
{"ambient", shader_param_default::ambient}, {"diffuse", shader_param_default::diffuse}, {"specular", shader_param_default::specular}, {"glossiness", shader_param_default::glossiness}};
|
||||
|
||||
void log_error(std::string const &Name, std::string const &Message)
|
||||
{
|
||||
ErrorLog("bad shader: " + Name + ": " + Message, logtype::shader);
|
||||
}
|
||||
|
||||
// the position of the next directive Magic ("texture", "param") of the materials: #pragma <magic>(...), or the older #<magic> (...)
|
||||
std::size_t find_directive(std::string const &Text, std::string const &Magic, std::size_t const From, std::size_t &Arguments)
|
||||
{
|
||||
auto start{From};
|
||||
while ((start = Text.find('#', start)) != std::string::npos)
|
||||
{
|
||||
auto position{start + 1};
|
||||
auto const skip_blanks = [&]() {
|
||||
while ((position < Text.size()) && ((Text[position] == ' ') || (Text[position] == '\t')))
|
||||
{
|
||||
++position;
|
||||
}
|
||||
};
|
||||
skip_blanks();
|
||||
if (Text.compare(position, 6, "pragma") == 0)
|
||||
{
|
||||
position += 6;
|
||||
skip_blanks();
|
||||
}
|
||||
if ((Text.compare(position, Magic.size(), Magic) == 0) && (position + Magic.size() < Text.size()))
|
||||
{
|
||||
position += Magic.size();
|
||||
skip_blanks();
|
||||
if ((position < Text.size()) && (Text[position] == '('))
|
||||
{
|
||||
Arguments = position;
|
||||
return start;
|
||||
}
|
||||
}
|
||||
++start;
|
||||
}
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
// the arguments of a directive, between the brackets, split at the commas; the directive is taken out of the text
|
||||
std::vector<std::string> take_directive(std::string &Text, std::size_t const Start, std::size_t const Arguments)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
auto const end{Text.find(')', Arguments)};
|
||||
if (end == std::string::npos)
|
||||
{
|
||||
Text.erase(Start, 1);
|
||||
return result;
|
||||
}
|
||||
std::istringstream stream(Text.substr(Arguments + 1, end - Arguments - 1));
|
||||
std::string token;
|
||||
while (std::getline(stream, token, ','))
|
||||
{
|
||||
std::istringstream tokenstream(token);
|
||||
std::string value;
|
||||
tokenstream >> value;
|
||||
result.emplace_back(value);
|
||||
}
|
||||
Text.erase(Start, end - Start + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
void parse_texture_entries(std::string &Text, shader_source &Source)
|
||||
{
|
||||
std::size_t start{0}, arguments{0};
|
||||
while ((start = find_directive(Text, "texture", start, arguments)) != std::string::npos)
|
||||
{
|
||||
auto const args{take_directive(Text, start, arguments)};
|
||||
if (args.size() != 3)
|
||||
{
|
||||
log_error(Source.name, "invalid argument count to #pragma texture");
|
||||
continue;
|
||||
}
|
||||
shader_texture_entry conf{};
|
||||
try
|
||||
{
|
||||
conf.id = static_cast<std::size_t>(std::stoul(args[1]));
|
||||
}
|
||||
catch (std::exception const &)
|
||||
{
|
||||
log_error(Source.name, "invalid texture binding: " + args[1]);
|
||||
continue;
|
||||
}
|
||||
auto const components{components_mapping.find(args[2])};
|
||||
if (components == components_mapping.end())
|
||||
{
|
||||
log_error(Source.name, "unknown components: " + args[2]);
|
||||
continue;
|
||||
}
|
||||
conf.components = components->second;
|
||||
if (args[0].empty())
|
||||
{
|
||||
log_error(Source.name, "empty name");
|
||||
}
|
||||
else if (conf.id >= MAX_SHADER_TEXTURES)
|
||||
{
|
||||
log_error(Source.name, "invalid texture binding: " + std::to_string(conf.id));
|
||||
}
|
||||
else
|
||||
{
|
||||
Source.texture_conf.try_emplace(args[0], conf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void parse_param_entries(std::string &Text, shader_source &Source)
|
||||
{
|
||||
std::size_t start{0}, arguments{0};
|
||||
while ((start = find_directive(Text, "param", start, arguments)) != std::string::npos)
|
||||
{
|
||||
auto const args{take_directive(Text, start, arguments)};
|
||||
if (args.size() != 5)
|
||||
{
|
||||
log_error(Source.name, "invalid argument count to #pragma param");
|
||||
continue;
|
||||
}
|
||||
shader_param_entry conf{};
|
||||
try
|
||||
{
|
||||
conf.location = static_cast<std::size_t>(std::stoul(args[1]));
|
||||
conf.offset = static_cast<std::size_t>(std::stoul(args[2]));
|
||||
conf.size = static_cast<std::size_t>(std::stoul(args[3]));
|
||||
}
|
||||
catch (std::exception const &)
|
||||
{
|
||||
log_error(Source.name, "invalid param: " + args[0]);
|
||||
continue;
|
||||
}
|
||||
auto const defaultparam{defaultparams_mapping.find(args[4])};
|
||||
if (defaultparam == defaultparams_mapping.end())
|
||||
{
|
||||
log_error(Source.name, "unknown param default: " + args[4]);
|
||||
continue;
|
||||
}
|
||||
conf.defaultparam = defaultparam->second;
|
||||
if (args[0].empty())
|
||||
{
|
||||
log_error(Source.name, "empty name");
|
||||
}
|
||||
else if (conf.location >= MAX_SHADER_PARAMS)
|
||||
{
|
||||
log_error(Source.name, "invalid param binding: " + std::to_string(conf.location));
|
||||
}
|
||||
else if (conf.offset > 3)
|
||||
{
|
||||
log_error(Source.name, "invalid offset: " + std::to_string(conf.offset));
|
||||
}
|
||||
else if (conf.offset + conf.size > 4)
|
||||
{
|
||||
log_error(Source.name, "invalid size: " + std::to_string(conf.size));
|
||||
}
|
||||
else
|
||||
{
|
||||
Source.param_conf.try_emplace(args[0], conf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t count_lines(std::string const &Text, std::size_t const End)
|
||||
{
|
||||
return static_cast<std::size_t>(std::count(Text.begin(), Text.begin() + std::min(End, Text.size()), '\n'));
|
||||
}
|
||||
|
||||
// the number of the file in the list of the files of the source, added when it isn't there
|
||||
std::size_t file_number(shader_source &Source, std::string const &File)
|
||||
{
|
||||
auto const lookup{std::find(Source.files.begin(), Source.files.end(), File)};
|
||||
if (lookup != Source.files.end())
|
||||
{
|
||||
return static_cast<std::size_t>(lookup - Source.files.begin());
|
||||
}
|
||||
Source.files.emplace_back(File);
|
||||
return Source.files.size() - 1;
|
||||
}
|
||||
|
||||
// replaces the includes of Text (the content of the file number Number) with the content of the files they name, with #line
|
||||
// directives which keep the numbers of the lines of each file in the messages of the compiler. an include is a line which
|
||||
// starts with #include (blanks before it allowed)
|
||||
void expand_includes(std::string &Text, std::size_t const Number, std::string const &Basedir, shader_source &Source, int const Depth)
|
||||
{
|
||||
if (Depth > 32)
|
||||
{
|
||||
throw shader_exception("includes nested too deep in " + Source.name);
|
||||
}
|
||||
std::string result;
|
||||
result.reserve(Text.size());
|
||||
std::size_t linestart{0};
|
||||
std::size_t linenumber{1};
|
||||
while (linestart < Text.size())
|
||||
{
|
||||
auto lineend{Text.find('\n', linestart)};
|
||||
if (lineend == std::string::npos)
|
||||
{
|
||||
lineend = Text.size();
|
||||
}
|
||||
std::string_view const line{Text.data() + linestart, lineend - linestart};
|
||||
auto position{line.find_first_not_of(" \t")};
|
||||
bool included{false};
|
||||
if ((position != std::string_view::npos) && (line.compare(position, 8, "#include") == 0))
|
||||
{
|
||||
position = line.find_first_not_of(" \t", position + 8);
|
||||
if (position != std::string_view::npos)
|
||||
{
|
||||
auto const open{line[position]};
|
||||
auto const close{(open == '<') ? '>' : (open == '"') ? '"' : '\0'};
|
||||
auto const end{(close != '\0') ? line.find(close, position + 1) : std::string_view::npos};
|
||||
if (end != std::string_view::npos)
|
||||
{
|
||||
std::string filename{line.substr(position + 1, end - position - 1)};
|
||||
if (filename == "common")
|
||||
{
|
||||
// the name the shaders used for the block of the game before it became a file
|
||||
filename = "common.glsl";
|
||||
}
|
||||
auto const number{file_number(Source, filename)};
|
||||
auto content{read_file(Basedir + filename)};
|
||||
expand_includes(content, number, Basedir, Source, Depth + 1);
|
||||
result += "#line 1 " + std::to_string(number) + "\n";
|
||||
result += content;
|
||||
if ((false == content.empty()) && (content.back() != '\n'))
|
||||
{
|
||||
result += '\n';
|
||||
}
|
||||
result += "#line " + std::to_string(linenumber + 1) + " " + std::to_string(Number);
|
||||
included = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (false == included)
|
||||
{
|
||||
result.append(line);
|
||||
}
|
||||
if (lineend < Text.size())
|
||||
{
|
||||
result += '\n';
|
||||
}
|
||||
linestart = lineend + 1;
|
||||
++linenumber;
|
||||
}
|
||||
Text = std::move(result);
|
||||
}
|
||||
|
||||
std::mutex manifest_mutex;
|
||||
|
||||
// the manifest of the binaries of a directory: binary -> (source file, hash)
|
||||
struct manifest_data
|
||||
{
|
||||
std::filesystem::file_time_type time{};
|
||||
bool found{false};
|
||||
std::unordered_map<std::string, std::vector<std::pair<std::string, std::uint64_t>>> entries;
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, manifest_data> manifests;
|
||||
|
||||
manifest_data const &manifest_of(std::string const &Directory)
|
||||
{
|
||||
auto &manifest{manifests[Directory]};
|
||||
auto const path{Directory + "manifest.txt"};
|
||||
std::error_code error;
|
||||
auto const time{std::filesystem::last_write_time(path, error)};
|
||||
if (error)
|
||||
{
|
||||
manifest = manifest_data{};
|
||||
return manifest;
|
||||
}
|
||||
if (manifest.found && (manifest.time == time))
|
||||
{
|
||||
return manifest;
|
||||
}
|
||||
manifest = manifest_data{};
|
||||
manifest.found = true;
|
||||
manifest.time = time;
|
||||
std::ifstream file(path);
|
||||
std::string line;
|
||||
while (std::getline(file, line))
|
||||
{
|
||||
if (line.empty() || (line[0] == '#'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::istringstream stream(line);
|
||||
std::string binary, source, hash;
|
||||
if (stream >> binary >> source >> hash)
|
||||
{
|
||||
try
|
||||
{
|
||||
manifest.entries[binary].emplace_back(source, std::stoull(hash, nullptr, 16));
|
||||
}
|
||||
catch (std::exception const &)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
char const *shader_api_name(shader_api const Api)
|
||||
{
|
||||
return (Api == shader_api::vulkan14 ? "vulkan 1.4" : "opengl 4.6");
|
||||
}
|
||||
|
||||
std::string shader_source_dir(shader_api const Api)
|
||||
{
|
||||
return (Api == shader_api::vulkan14 ? "shaders/vk14/" : "shaders/ogl46/");
|
||||
}
|
||||
|
||||
std::string shader_binary_dir(shader_api const Api)
|
||||
{
|
||||
return (Api == shader_api::vulkan14 ? "shaders/vk14bin/" : "shaders/ogl46bin/");
|
||||
}
|
||||
|
||||
shader_stage shader_stage_of(std::string const &Name)
|
||||
{
|
||||
if (ends_with(Name, ".vert"))
|
||||
{
|
||||
return shader_stage::vertex;
|
||||
}
|
||||
if (ends_with(Name, ".frag"))
|
||||
{
|
||||
return shader_stage::fragment;
|
||||
}
|
||||
if (ends_with(Name, ".geom"))
|
||||
{
|
||||
return shader_stage::geometry;
|
||||
}
|
||||
if (ends_with(Name, ".comp"))
|
||||
{
|
||||
return shader_stage::compute;
|
||||
}
|
||||
throw shader_exception("unknown shader " + Name);
|
||||
}
|
||||
|
||||
char const *shader_stage_define(shader_stage const Stage)
|
||||
{
|
||||
switch (Stage)
|
||||
{
|
||||
case shader_stage::vertex:
|
||||
return "STAGE_VERTEX";
|
||||
case shader_stage::fragment:
|
||||
return "STAGE_FRAGMENT";
|
||||
case shader_stage::geometry:
|
||||
return "STAGE_GEOMETRY";
|
||||
case shader_stage::compute:
|
||||
return "STAGE_COMPUTE";
|
||||
}
|
||||
return "STAGE_VERTEX";
|
||||
}
|
||||
|
||||
std::string shader_binary_name(std::string const &Name, std::string const &Variant)
|
||||
{
|
||||
return Name + (Variant.empty() ? "" : "." + Variant) + ".spv";
|
||||
}
|
||||
|
||||
std::string shader_variant_of(std::string const &Defines, bool &Plain)
|
||||
{
|
||||
Plain = true;
|
||||
std::string result;
|
||||
std::istringstream stream(Defines);
|
||||
std::string line;
|
||||
while (std::getline(stream, line))
|
||||
{
|
||||
std::istringstream linestream(line);
|
||||
std::string directive, name, value, rest;
|
||||
if (false == static_cast<bool>(linestream >> directive))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ((directive != "#define") || (false == static_cast<bool>(linestream >> name)))
|
||||
{
|
||||
Plain = false;
|
||||
continue;
|
||||
}
|
||||
linestream >> value;
|
||||
if ((false == value.empty()) && (value != "1"))
|
||||
{
|
||||
Plain = false;
|
||||
}
|
||||
if (linestream >> rest)
|
||||
{
|
||||
Plain = false;
|
||||
}
|
||||
result += (result.empty() ? "" : ".") + name;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::uint64_t shader_file_hash(std::string_view const Text)
|
||||
{
|
||||
std::uint64_t hash{14695981039346656037ull};
|
||||
for (std::size_t i = 0; i < Text.size(); ++i)
|
||||
{
|
||||
auto const c{Text[i]};
|
||||
if ((c == '\r') && (i + 1 < Text.size()) && (Text[i + 1] == '\n'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
hash ^= static_cast<std::uint8_t>(c);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
shader_source preprocess_shader(shader_api const Api, std::string const &Name, std::string const &Defines, std::string const &Preamble, bool const Allowmissing, std::string const &Basedir)
|
||||
{
|
||||
shader_source source;
|
||||
source.api = Api;
|
||||
source.name = Name;
|
||||
source.stage = shader_stage_of(Name);
|
||||
bool plain{true};
|
||||
source.variant = shader_variant_of(Defines, plain);
|
||||
source.has_binary_name = plain;
|
||||
|
||||
auto const basedir{Basedir.empty() ? shader_source_dir(Api) : Basedir};
|
||||
bool found{false};
|
||||
auto text{read_text(basedir + Name, found)};
|
||||
if ((false == found) || text.empty())
|
||||
{
|
||||
if (Allowmissing)
|
||||
{
|
||||
source.files.emplace_back(Name);
|
||||
return source;
|
||||
}
|
||||
throw shader_exception("cannot read shader: " + basedir + Name);
|
||||
}
|
||||
source.has_source = true;
|
||||
source.files.emplace_back(Name);
|
||||
|
||||
// what goes after #version: the variant, the renderer, the stage where the file doesn't say it
|
||||
std::string preamble{Defines};
|
||||
if ((false == preamble.empty()) && (preamble.back() != '\n'))
|
||||
{
|
||||
preamble += '\n';
|
||||
}
|
||||
preamble += Preamble;
|
||||
if ((false == preamble.empty()) && (preamble.back() != '\n'))
|
||||
{
|
||||
preamble += '\n';
|
||||
}
|
||||
if (text.find(std::string("#define ") + shader_stage_define(source.stage)) == std::string::npos)
|
||||
{
|
||||
preamble += std::string("#define ") + shader_stage_define(source.stage) + " 1\n";
|
||||
}
|
||||
|
||||
// the #version line, which has to stay first
|
||||
std::size_t versionend{std::string::npos};
|
||||
{
|
||||
auto version{text.find("#version")};
|
||||
if ((version != std::string::npos) && ((version == 0) || (text[version - 1] == '\n')))
|
||||
{
|
||||
versionend = text.find('\n', version);
|
||||
}
|
||||
}
|
||||
expand_includes(text, 0, basedir, source, 0);
|
||||
parse_texture_entries(text, source);
|
||||
parse_param_entries(text, source);
|
||||
|
||||
if (versionend != std::string::npos)
|
||||
{
|
||||
// the includes come after the #version line, which is where it was
|
||||
versionend = text.find('\n', text.find("#version"));
|
||||
auto const line{count_lines(text, versionend) + 2};
|
||||
text.insert(versionend + 1, preamble + "#line " + std::to_string(line) + " 0\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
text = "#version 460 core\n" + preamble + "#line 1 0\n" + text;
|
||||
}
|
||||
source.text = std::move(text);
|
||||
return source;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> load_shader_binary(shader_source const &Source, std::string &Reason)
|
||||
{
|
||||
Reason.clear();
|
||||
if (false == Source.has_binary_name)
|
||||
{
|
||||
Reason = "the variant isn't one of plain defines";
|
||||
return {};
|
||||
}
|
||||
auto const directory{shader_binary_dir(Source.api)};
|
||||
auto const name{shader_binary_name(Source.name, Source.variant)};
|
||||
bool found{false};
|
||||
auto const data{read_text(directory + name, found)};
|
||||
if (false == found)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
if ((data.size() < 20) || (data.size() % 4 != 0))
|
||||
{
|
||||
Reason = name + " isn't SPIR-V";
|
||||
return {};
|
||||
}
|
||||
std::vector<std::uint32_t> words(data.size() / 4);
|
||||
std::memcpy(words.data(), data.data(), data.size());
|
||||
if (words[0] != 0x07230203u)
|
||||
{
|
||||
Reason = name + " isn't SPIR-V";
|
||||
return {};
|
||||
}
|
||||
// the sources the binary was compiled from, as the manifest has them: a binary older than its sources isn't taken
|
||||
std::lock_guard<std::mutex> lock(manifest_mutex);
|
||||
auto const &manifest{manifest_of(directory)};
|
||||
auto const entry{manifest.entries.find(name)};
|
||||
if (entry != manifest.entries.end())
|
||||
{
|
||||
auto const sourcedir{shader_source_dir(Source.api)};
|
||||
for (auto const &[file, hash] : entry->second)
|
||||
{
|
||||
bool sourcefound{false};
|
||||
auto const text{read_text(sourcedir + file, sourcefound)};
|
||||
if (false == sourcefound)
|
||||
{
|
||||
// the sources aren't there, so they can't be newer
|
||||
continue;
|
||||
}
|
||||
if (shader_file_hash(text) != hash)
|
||||
{
|
||||
Reason = name + " was compiled from another " + file + " than the one in " + sourcedir;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> spirv_specialization_ids(std::vector<std::uint32_t> const &Words)
|
||||
{
|
||||
std::vector<std::uint32_t> result;
|
||||
// the header is 5 words; each instruction starts with its word count (high 16 bits) and opcode (low 16 bits)
|
||||
std::size_t index{5};
|
||||
while (index < Words.size())
|
||||
{
|
||||
auto const count{Words[index] >> 16};
|
||||
auto const opcode{Words[index] & 0xffffu};
|
||||
if ((count == 0) || (index + count > Words.size()))
|
||||
{
|
||||
break;
|
||||
}
|
||||
// OpDecorate %target SpecId <id>
|
||||
if ((opcode == 71) && (count >= 4) && (Words[index + 2] == 1))
|
||||
{
|
||||
result.push_back(Words[index + 3]);
|
||||
}
|
||||
index += count;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> shader_file_defines(shader_api const Api, std::string const &Name, std::string const &Basedir)
|
||||
{
|
||||
std::unordered_map<std::string, std::string> result;
|
||||
bool found{false};
|
||||
auto const text{read_text((Basedir.empty() ? shader_source_dir(Api) : Basedir) + Name, found)};
|
||||
std::istringstream stream(text);
|
||||
std::string line;
|
||||
while (std::getline(stream, line))
|
||||
{
|
||||
std::istringstream linestream(line);
|
||||
std::string directive, name, value;
|
||||
if ((linestream >> directive >> name) && (directive == "#define"))
|
||||
{
|
||||
std::getline(linestream, value);
|
||||
auto const first{value.find_first_not_of(" \t")};
|
||||
auto const last{value.find_last_not_of(" \t\r")};
|
||||
result[name] = (first == std::string::npos ? std::string{} : value.substr(first, last - first + 1));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool shader_constants_match(shader_api const Api, std::string const &Name, std::string const &Preamble, std::string const &Basedir, std::vector<std::string> &Differences)
|
||||
{
|
||||
Differences.clear();
|
||||
auto const file{shader_file_defines(Api, Name, Basedir)};
|
||||
if (file.empty())
|
||||
{
|
||||
Differences.emplace_back(Name + " can't be read");
|
||||
return false;
|
||||
}
|
||||
// the same number however it's written: 0.2 and 0.200000, 3 and 3U
|
||||
auto const normalized = [](std::string Value) {
|
||||
if ((false == Value.empty()) && ((Value.back() == 'U') || (Value.back() == 'u')))
|
||||
{
|
||||
Value.pop_back();
|
||||
}
|
||||
try
|
||||
{
|
||||
std::size_t used{0};
|
||||
auto const number{std::stod(Value, &used)};
|
||||
if (used == Value.size())
|
||||
{
|
||||
return std::to_string(number);
|
||||
}
|
||||
}
|
||||
catch (std::exception const &)
|
||||
{
|
||||
}
|
||||
return Value;
|
||||
};
|
||||
std::istringstream stream(Preamble);
|
||||
std::string line;
|
||||
while (std::getline(stream, line))
|
||||
{
|
||||
std::istringstream linestream(line);
|
||||
std::string directive, name, value;
|
||||
if (!(linestream >> directive >> name >> value) || (directive != "#define"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto const lookup{file.find(name)};
|
||||
if (lookup == file.end())
|
||||
{
|
||||
Differences.emplace_back(name + " is missing");
|
||||
}
|
||||
else if (normalized(lookup->second) != normalized(value))
|
||||
{
|
||||
Differences.emplace_back(name + " is " + lookup->second + " in the file and " + value + " in the game");
|
||||
}
|
||||
}
|
||||
return Differences.empty();
|
||||
}
|
||||
|
||||
} // namespace gfx
|
||||
173
rendering/common/shadersource.h
Normal file
173
rendering/common/shadersource.h
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
// the shader files of the renderers, whichever api compiles them: where they are, how a file is made ready to compile (its
|
||||
// includes, the directives of the materials, what the renderer puts before the source), and the SPIR-V binaries compiled from
|
||||
// them ahead of time, which the renderers load before they compile the source.
|
||||
//
|
||||
// each api has a directory of sources and one of binaries: shaders/ogl46/ and shaders/ogl46bin/ for opengl 4.6,
|
||||
// shaders/vk14/ and shaders/vk14bin/ for vulkan 1.4. a binary is named after its source, <file>.spv, and a variant of the
|
||||
// shader (the defines of the renderer, e.g. MODEL_DRAWS) <file>.<DEFINE>.spv. the sources can be compiled by glslc as they are
|
||||
// (shaders/compile_spirv.py), which also writes the manifest of the binaries: the sources each binary was compiled from, so a
|
||||
// binary older than its sources isn't taken.
|
||||
//
|
||||
// no graphics api here: the file is also built into the shader validator (ci_shadervalidator)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace gfx
|
||||
{
|
||||
|
||||
class shader_exception : public std::runtime_error
|
||||
{
|
||||
using runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
enum class shader_api
|
||||
{
|
||||
opengl46,
|
||||
vulkan14
|
||||
};
|
||||
|
||||
enum class shader_stage
|
||||
{
|
||||
vertex,
|
||||
fragment,
|
||||
geometry,
|
||||
compute
|
||||
};
|
||||
|
||||
// textures and parameters a material shader can take, the same as gl::MAX_TEXTURES and gl::MAX_PARAMS
|
||||
constexpr std::size_t MAX_SHADER_TEXTURES = 8;
|
||||
constexpr std::size_t MAX_SHADER_PARAMS = 3;
|
||||
|
||||
// what the texture of a unit holds, as #pragma texture(name, unit, components) gives it. the values are those of opengl
|
||||
// (GL_RED ... GL_SRGB_ALPHA), which the textures take as the hint of their format
|
||||
enum class shader_components : int
|
||||
{
|
||||
R = 0x1903,
|
||||
RG = 0x8227,
|
||||
RGB = 0x1907,
|
||||
RGBA = 0x1908,
|
||||
sRGB = 0x8C40,
|
||||
sRGB_A = 0x8C42
|
||||
};
|
||||
|
||||
struct shader_texture_entry
|
||||
{
|
||||
std::size_t id;
|
||||
shader_components components;
|
||||
};
|
||||
|
||||
enum class shader_param_default
|
||||
{
|
||||
required,
|
||||
nan,
|
||||
zero,
|
||||
one,
|
||||
ambient,
|
||||
diffuse,
|
||||
specular,
|
||||
glossiness
|
||||
};
|
||||
|
||||
// #pragma param(name, location, offset, size, default): a part of one of the vec4 parameters of the model block
|
||||
struct shader_param_entry
|
||||
{
|
||||
std::size_t location;
|
||||
std::size_t offset;
|
||||
std::size_t size;
|
||||
shader_param_default defaultparam;
|
||||
};
|
||||
|
||||
using shader_texture_conf = std::unordered_map<std::string, shader_texture_entry>;
|
||||
using shader_param_conf = std::unordered_map<std::string, shader_param_entry>;
|
||||
|
||||
// a program of a material as the materials see it, whichever api draws it: the textures and the parameters it takes, the
|
||||
// name it was fetched by (gfx_renderer::Fetch_Shader)
|
||||
class shader_program
|
||||
{
|
||||
public:
|
||||
virtual ~shader_program() = default;
|
||||
|
||||
shader_texture_conf texture_conf;
|
||||
shader_param_conf param_conf;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// a shader file made ready to compile
|
||||
struct shader_source
|
||||
{
|
||||
shader_api api{shader_api::opengl46};
|
||||
shader_stage stage{shader_stage::vertex};
|
||||
// the file as asked for, e.g. mat_default.frag
|
||||
std::string name;
|
||||
// the names of the defines of the variant joined with dots (MODEL_DRAWS), empty for the shader itself. a variant whose
|
||||
// defines aren't all plain flags (#define NAME or #define NAME 1) has no binary: invalid_variant
|
||||
std::string variant;
|
||||
bool has_binary_name{true};
|
||||
// whether the source file is there; a binary can be loaded without it, but the material directives come from the source
|
||||
bool has_source{false};
|
||||
// the source with the preamble put after #version and the includes expanded, for a compiler of the source. the files are
|
||||
// told apart in the messages of the compiler by their numbers in files (#line <line> <number>)
|
||||
std::string text;
|
||||
// the file and its includes, relative to the directory of the sources, by their numbers
|
||||
std::vector<std::string> files;
|
||||
shader_texture_conf texture_conf;
|
||||
shader_param_conf param_conf;
|
||||
};
|
||||
|
||||
// "opengl 4.6", "vulkan 1.4"
|
||||
char const *shader_api_name(shader_api Api);
|
||||
// the directories of the sources and of the binaries of an api: shaders/ogl46/, shaders/ogl46bin/ ...
|
||||
std::string shader_source_dir(shader_api Api);
|
||||
std::string shader_binary_dir(shader_api Api);
|
||||
// the stage of a file by its extension (.vert, .frag, .geom, .comp). throws shader_exception for another one
|
||||
shader_stage shader_stage_of(std::string const &Name);
|
||||
// the define the files set for their stage: STAGE_VERTEX ...
|
||||
char const *shader_stage_define(shader_stage Stage);
|
||||
// the file name of the binary of a shader: mat_default.frag.spv, vertex.vert.MODEL_DRAWS.spv
|
||||
std::string shader_binary_name(std::string const &Name, std::string const &Variant);
|
||||
// the variant named by the defines of a renderer ("#define MODEL_DRAWS 1\n" -> MODEL_DRAWS). Plain: false when a define isn't
|
||||
// a plain flag (the variant can't have a binary then)
|
||||
std::string shader_variant_of(std::string const &Defines, bool &Plain);
|
||||
// the hash the manifest of the binaries keeps of a source file: fnv-1a 64 of the text with the line ends made \n
|
||||
std::uint64_t shader_file_hash(std::string_view Text);
|
||||
|
||||
// reads the shader Name of Api and makes it ready to compile: the includes expanded (#include "file", #include <file>; <common>
|
||||
// stands for common.glsl), #pragma texture(...) and #pragma param(...) read and taken out of the text, and after the #version
|
||||
// line (#version 460 core put first when the file has none) the defines of the variant, the preamble of the renderer and,
|
||||
// where the file doesn't set it, the define of its stage. Basedir: the directory of the sources, shader_source_dir() when empty.
|
||||
// a missing file is an error unless Allowmissing (the source then comes back with has_source false). throws shader_exception
|
||||
shader_source preprocess_shader(shader_api Api, std::string const &Name, std::string const &Defines, std::string const &Preamble, bool const Allowmissing = false,
|
||||
std::string const &Basedir = {});
|
||||
|
||||
// the SPIR-V binary of a shader from the directory of the binaries of its api. empty when there's none, when the manifest of the
|
||||
// binaries says the binary was compiled from other sources than those there now, or when the file isn't SPIR-V; Reason says
|
||||
// which (empty when there's simply no binary)
|
||||
std::vector<std::uint32_t> load_shader_binary(shader_source const &Source, std::string &Reason);
|
||||
|
||||
// the ids of the specialization constants of a SPIR-V module (OpDecorate ... SpecId)
|
||||
std::vector<std::uint32_t> spirv_specialization_ids(std::vector<std::uint32_t> const &Words);
|
||||
|
||||
// the #define lines of a source, NAME -> value as written, for the checks of the constants the binaries were compiled with
|
||||
std::unordered_map<std::string, std::string> shader_file_defines(shader_api Api, std::string const &Name, std::string const &Basedir = {});
|
||||
// whether the constants a renderer puts before the sources (the #define lines of Preamble) are those the file Name of Api writes
|
||||
// for itself (the binaries are compiled by glslc with the values of the file). Differences: what isn't the same
|
||||
bool shader_constants_match(shader_api Api, std::string const &Name, std::string const &Preamble, std::string const &Basedir, std::vector<std::string> &Differences);
|
||||
|
||||
} // namespace gfx
|
||||
Reference in New Issue
Block a user