#include "Shader.hpp" #include "AppConfig.hpp" #include "RapidJsonHelper.hpp" #include "ScopeGuard.hpp" #include "Utils.hpp" #include #include #include #include #include #include #include #include namespace fs = std::filesystem; using namespace std::literals; bool ShaderInfo::SaveToFile(const fs::path& filePath) const { rapidjson::Document root(rapidjson::kObjectType); auto SaveInputOutputThings = [&](const char* name, const std::vector& things) { rapidjson::Value rvThings(rapidjson::kArrayType); for (auto& thing : things) { rapidjson::Value rvThing(rapidjson::kObjectType); rvThing.AddMember("Name", thing.variable.name, root.GetAllocator()); rvThing.AddMember("Semantic", rapidjson::StringRef(Tags::NameOf(thing.semantic)), root.GetAllocator()); rvThing.AddMember("ScalarType", rapidjson::StringRef(Tags::NameOfGLType(thing.variable.scalarType)), root.GetAllocator()); rvThing.AddMember("Width", thing.variable.width, root.GetAllocator()); rvThing.AddMember("Height", thing.variable.height, root.GetAllocator()); rvThing.AddMember("ArrayLength", thing.variable.arrayLength, root.GetAllocator()); rvThing.AddMember("OpenGLLocation", thing.variable.location, root.GetAllocator()); rvThings.PushBack(rvThing, root.GetAllocator()); } root.AddMember(rapidjson::StringRef(name), rvThings, root.GetAllocator()); }; SaveInputOutputThings("Inputs", inputs); SaveInputOutputThings("Outputs", outputs); // TODO uniforms auto file = Utils::OpenCstdioFile(filePath, Utils::WriteTruncate); if (!file) return false; DEFER { fclose(file); }; char writerBuffer[65536]; rapidjson::FileWriteStream stream(file, writerBuffer, sizeof(writerBuffer)); rapidjson::Writer writer(stream); root.Accept(writer); return true; } bool ShaderInfo::LoadFromFile(const fs::path& filePath) { auto file = Utils::OpenCstdioFile(filePath, Utils::Read); if (!file) return false; DEFER { fclose(file); }; char readerBuffer[65536]; rapidjson::FileReadStream stream(file, readerBuffer, sizeof(readerBuffer)); rapidjson::Document root; root.ParseStream(stream); auto LoadInputOutputThings = [&](std::string_view name, std::vector& things) { auto rvThings = rapidjson::GetProperty(root, rapidjson::kObjectType, name); if (!rvThings) return; if (!rvThings->IsArray()) return; for (auto& elm : rvThings->GetArray()) { if (!elm.IsObject()) continue; InputOutputThing thing; BRUSSEL_JSON_GET(elm, "Name", std::string, thing.variable.name, continue); { // Semantic auto value = rapidjson::GetProperty(elm, rapidjson::kStringType, "Semantic"sv); if (!value) { thing.semantic = Tags::VES_Generic; } else { thing.semantic = Tags::FindVertexElementSemantic(rapidjson::AsStringView(*value)); } } { // Scalar type auto value = rapidjson::GetProperty(elm, rapidjson::kStringType, "ScalarType"sv); if (!value) continue; thing.variable.scalarType = Tags::FindGLType(rapidjson::AsStringView(*value)); } BRUSSEL_JSON_GET(elm, "Width", int, thing.variable.width, continue); BRUSSEL_JSON_GET(elm, "Height", int, thing.variable.height, continue); BRUSSEL_JSON_GET(elm, "OpenGLLocation", int, thing.variable.location, continue); BRUSSEL_JSON_GET_DEFAULT(elm, "ArrayLength", int, thing.variable.arrayLength, 1); things.push_back(std::move(thing)); } }; LoadInputOutputThings("Inputs"sv, inputs); LoadInputOutputThings("Outputs"sv, outputs); // TODO uniforms return true; } Shader::Shader(std::string name) : mName{ name } { } Shader::~Shader() { glDeleteProgram(mHandle); } namespace ProjectBrussel_UNITY_ID { // Grabs section [begin, end) Shader::ErrorCode CreateShader(GLuint& out, const char* src, int beginIdx, int endIdx, GLenum type) { out = glCreateShader(type); const GLchar* begin = &src[beginIdx]; const GLint len = endIdx - beginIdx; glShaderSource(out, 1, &begin, &len); glCompileShader(out); GLint compileStatus; glGetShaderiv(out, GL_COMPILE_STATUS, &compileStatus); if (compileStatus == GL_FALSE) { GLint len; glGetShaderiv(out, GL_INFO_LOG_LENGTH, &len); std::string log(len, '\0'); glGetShaderInfoLog(out, len, nullptr, log.data()); return Shader ::CompilationFailed; } return Shader::Success; } Shader::ErrorCode CreateShader(GLuint& out, std::string_view str, GLenum type) { return CreateShader(out, str.data(), 0, str.size(), type); } Shader::ErrorCode LinkShaderProgram(GLuint program) { glLinkProgram(program); GLint linkStatus; glGetProgramiv(program, GL_LINK_STATUS, &linkStatus); if (linkStatus == GL_FALSE) { GLint len; glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len); std::string log(len, '\0'); glGetProgramInfoLog(program, len, nullptr, log.data()); return Shader::LinkingFailed; } return Shader::Success; } } // namespace ProjectBrussel_UNITY_ID #define CATCH_ERROR_IMPL(x, name) \ auto name = x; \ if (name != Shader::Success) { \ return name; \ } #define CATCH_ERROR(x) CATCH_ERROR_IMPL(x, UNIQUE_NAME(result)) Shader::ErrorCode Shader::InitFromSources(const ShaderSources& sources) { using namespace ProjectBrussel_UNITY_ID; if (IsValid()) { return ShaderAlreadyCreated; } GLuint program = glCreateProgram(); ScopeGuard sg = [&]() { glDeleteProgram(program); }; GLuint vertex = 0; DEFER { glDeleteShader(vertex); }; if (!sources.vertex.empty()) { CATCH_ERROR(CreateShader(vertex, sources.vertex, GL_VERTEX_SHADER)); glAttachShader(program, vertex); } GLuint geometry = 0; DEFER { glDeleteShader(geometry); }; if (!sources.geometry.empty()) { CATCH_ERROR(CreateShader(geometry, sources.geometry, GL_GEOMETRY_SHADER)); glAttachShader(program, geometry); } GLuint tessControl = 0; DEFER { glDeleteShader(tessControl); }; if (!sources.tessControl.empty()) { CATCH_ERROR(CreateShader(tessControl, sources.tessControl, GL_TESS_CONTROL_SHADER)); glAttachShader(program, tessControl); } GLuint tessEval = 0; DEFER { glDeleteShader(tessEval); }; if (!sources.tessEval.empty()) { CATCH_ERROR(CreateShader(tessEval, sources.tessEval, GL_TESS_EVALUATION_SHADER)); glAttachShader(program, tessEval); } GLuint fragment = 0; DEFER { glDeleteShader(fragment); }; if (!sources.fragment.empty()) { CATCH_ERROR(CreateShader(fragment, sources.fragment, GL_FRAGMENT_SHADER)); glAttachShader(program, fragment); } CATCH_ERROR(LinkShaderProgram(program)); sg.Dismiss(); mHandle = program; return Success; } Shader::ErrorCode Shader::InitFromSource(std::string_view source) { using namespace ProjectBrussel_UNITY_ID; GLuint vertex = 0; DEFER { glDeleteShader(vertex); }; GLuint geometry = 0; DEFER { glDeleteShader(geometry); }; GLuint tessControl = 0; DEFER { glDeleteShader(tessControl); }; GLuint tessEval = 0; DEFER { glDeleteShader(tessEval); }; GLuint fragment = 0; DEFER { glDeleteShader(fragment); }; int prevBegin = -1; // Excluding #type marker int prevEnd = -1; // [begin, end) std::string prevShaderVariant; auto CommitSection = [&]() -> ErrorCode { if (prevBegin == -1 || prevEnd == -1) { // Not actually "succeeding" here, but we just want to skip this call and continue return Success; } if (prevShaderVariant == "vertex" && !vertex) { CATCH_ERROR(CreateShader(vertex, source.data(), prevBegin, prevEnd, GL_VERTEX_SHADER)); } else if (prevShaderVariant == "geometry" && !geometry) { CATCH_ERROR(CreateShader(geometry, source.data(), prevBegin, prevEnd, GL_GEOMETRY_SHADER)); } else if (prevShaderVariant == "tessellation_control" && !tessControl) { CATCH_ERROR(CreateShader(tessControl, source.data(), prevBegin, prevEnd, GL_TESS_CONTROL_SHADER)); } else if (prevShaderVariant == "tessellation_evaluation" && !tessEval) { CATCH_ERROR(CreateShader(tessEval, source.data(), prevBegin, prevEnd, GL_TESS_EVALUATION_SHADER)); } else if (prevShaderVariant == "fragment" && !fragment) { CATCH_ERROR(CreateShader(fragment, source.data(), prevBegin, prevEnd, GL_FRAGMENT_SHADER)); } else { return InvalidShaderVariant; } prevBegin = -1; prevEnd = -1; prevShaderVariant.clear(); return Success; }; constexpr const char* kMarker = "#type "; bool matchingDirective = true; // If true, we are matching marker pattern; if false, we are accumulating shader variant identifier int matchIndex = 0; // Current index of the pattern trying to match std::string shaderVariant; // Don't use utf8 iterator, shader sources are expected to be ASCII only for (size_t i = 0; i < source.size(); ++i) { char c = source[i]; if (matchingDirective) { if (c == kMarker[matchIndex]) { // Matched the expected character, go to next char in pattern matchIndex++; // If we are at the end of the marker pattern... if (kMarker[matchIndex] == '\0') { matchingDirective = false; matchIndex = 0; continue; } // This might be a shader variant directive -> might be end of a section if (c == '#') { prevEnd = i; continue; } } else { // Unexpected character, rollback to beginning matchIndex = 0; } } else { if (c == '\n') { // Found complete shader variant directive CATCH_ERROR(CommitSection()); // Try commit section, for the first apparent of #type this should do nothing, as `prevEnd` will still be -1 prevBegin = i + 1; // +1 to skip new line (technically not needed) prevShaderVariant = std::move(shaderVariant); matchingDirective = true; shaderVariant.clear(); } else { // Simply accumulate to shader variant buffer shaderVariant += c; } } } // Commit the last section prevEnd = static_cast(source.size()); CATCH_ERROR(CommitSection()); GLuint program = glCreateProgram(); ScopeGuard sg = [&]() { glDeleteProgram(program); }; if (vertex) glAttachShader(program, vertex); if (geometry) glAttachShader(program, geometry); if (tessControl) glAttachShader(program, tessControl); if (tessEval) glAttachShader(program, tessEval); if (fragment) glAttachShader(program, fragment); CATCH_ERROR(LinkShaderProgram(program)); sg.Dismiss(); mHandle = program; return Success; } #undef CATCH_ERROR namespace ProjectBrussel_UNITY_ID { bool QueryMathInfo(GLenum type, GLenum& scalarType, int& width, int& height) { auto DoOutput = [&](GLenum scalarTypeIn, int widthIn, int heightIn) { width = widthIn; height = heightIn; scalarType = scalarTypeIn; }; switch (type) { case GL_FLOAT: case GL_DOUBLE: case GL_INT: case GL_UNSIGNED_INT: case GL_BOOL: { DoOutput(type, 1, 1); return true; } case GL_FLOAT_VEC2: DoOutput(GL_FLOAT, 1, 2); return true; case GL_FLOAT_VEC3: DoOutput(GL_FLOAT, 1, 3); return true; case GL_FLOAT_VEC4: DoOutput(GL_FLOAT, 1, 4); return true; case GL_DOUBLE_VEC2: DoOutput(GL_DOUBLE, 1, 2); return true; case GL_DOUBLE_VEC3: DoOutput(GL_DOUBLE, 1, 3); return true; case GL_DOUBLE_VEC4: DoOutput(GL_DOUBLE, 1, 4); return true; case GL_INT_VEC2: DoOutput(GL_INT, 1, 2); return true; case GL_INT_VEC3: DoOutput(GL_INT, 1, 3); return true; case GL_INT_VEC4: DoOutput(GL_INT, 1, 4); return true; case GL_UNSIGNED_INT_VEC2: DoOutput(GL_UNSIGNED_INT, 1, 2); return true; case GL_UNSIGNED_INT_VEC3: DoOutput(GL_UNSIGNED_INT, 1, 3); return true; case GL_UNSIGNED_INT_VEC4: DoOutput(GL_UNSIGNED_INT, 1, 4); return true; case GL_BOOL_VEC2: DoOutput(GL_BOOL, 1, 2); return true; case GL_BOOL_VEC3: DoOutput(GL_BOOL, 1, 3); return true; case GL_BOOL_VEC4: DoOutput(GL_BOOL, 1, 4); return true; case GL_FLOAT_MAT2: DoOutput(GL_FLOAT, 2, 2); return true; case GL_FLOAT_MAT3: DoOutput(GL_FLOAT, 3, 3); return true; case GL_FLOAT_MAT4: DoOutput(GL_FLOAT, 4, 4); return true; case GL_FLOAT_MAT2x3: DoOutput(GL_FLOAT, 2, 3); return true; case GL_FLOAT_MAT2x4: DoOutput(GL_FLOAT, 2, 4); return true; case GL_FLOAT_MAT3x2: DoOutput(GL_FLOAT, 3, 2); return true; case GL_FLOAT_MAT3x4: DoOutput(GL_FLOAT, 3, 4); return true; case GL_FLOAT_MAT4x2: DoOutput(GL_FLOAT, 4, 2); return true; case GL_FLOAT_MAT4x3: DoOutput(GL_FLOAT, 4, 3); return true; case GL_DOUBLE_MAT2: DoOutput(GL_DOUBLE, 2, 2); return true; case GL_DOUBLE_MAT3: DoOutput(GL_DOUBLE, 3, 3); return true; case GL_DOUBLE_MAT4: DoOutput(GL_DOUBLE, 4, 4); return true; case GL_DOUBLE_MAT2x3: DoOutput(GL_DOUBLE, 2, 3); return true; case GL_DOUBLE_MAT2x4: DoOutput(GL_DOUBLE, 2, 4); return true; case GL_DOUBLE_MAT3x2: DoOutput(GL_DOUBLE, 3, 2); return true; case GL_DOUBLE_MAT3x4: DoOutput(GL_DOUBLE, 3, 4); return true; case GL_DOUBLE_MAT4x2: DoOutput(GL_DOUBLE, 4, 2); return true; case GL_DOUBLE_MAT4x3: DoOutput(GL_DOUBLE, 4, 3); return true; } return false; } bool QuerySamplerInfo(GLenum type) { switch (type) { case GL_SAMPLER_1D: case GL_SAMPLER_2D: case GL_SAMPLER_3D: case GL_SAMPLER_CUBE: case GL_SAMPLER_1D_SHADOW: case GL_SAMPLER_2D_SHADOW: case GL_SAMPLER_1D_ARRAY: case GL_SAMPLER_2D_ARRAY: case GL_SAMPLER_1D_ARRAY_SHADOW: case GL_SAMPLER_2D_ARRAY_SHADOW: case GL_SAMPLER_2D_MULTISAMPLE: case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: case GL_SAMPLER_CUBE_SHADOW: case GL_SAMPLER_BUFFER: case GL_SAMPLER_2D_RECT: case GL_SAMPLER_2D_RECT_SHADOW: case GL_INT_SAMPLER_1D: case GL_INT_SAMPLER_2D: case GL_INT_SAMPLER_3D: case GL_INT_SAMPLER_CUBE: case GL_INT_SAMPLER_1D_ARRAY: case GL_INT_SAMPLER_2D_ARRAY: case GL_INT_SAMPLER_2D_MULTISAMPLE: case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: case GL_INT_SAMPLER_BUFFER: case GL_INT_SAMPLER_2D_RECT: case GL_UNSIGNED_INT_SAMPLER_1D: case GL_UNSIGNED_INT_SAMPLER_2D: case GL_UNSIGNED_INT_SAMPLER_3D: case GL_UNSIGNED_INT_SAMPLER_CUBE: case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: case GL_UNSIGNED_INT_SAMPLER_BUFFER: case GL_UNSIGNED_INT_SAMPLER_2D_RECT: return true; } return false; } std::unique_ptr CreateVariable(GLenum type, GLuint loc) { GLenum scalarType; int width; int height; if (QueryMathInfo(type, scalarType, width, height)) { auto res = std::make_unique(); res->location = loc; res->scalarType = type; res->width = width; res->height = height; return res; } if (QuerySamplerInfo(type)) { auto res = std::make_unique(); res->location = loc; res->type = type; return res; } return nullptr; } } // namespace ProjectBrussel_UNITY_ID bool Shader::CreateEmptyInfoIfAbsent() { if (mInfo || !IsValid()) { return false; } mInfo = std::make_unique(); return true; } bool Shader::GatherInfoIfAbsent() { using namespace ProjectBrussel_UNITY_ID; using ThingId = ShaderInfo::ThingId; if (mInfo || !IsValid()) { return false; } mInfo = std::make_unique(); // TODO handle differnt types of variables with the same name // TODO work with OpenGL < 4.3, possibly with glslang #if 0 int inputCount; glGetProgramInterfaceiv(mHandle, GL_PROGRAM_INPUT, GL_ACTIVE_RESOURCES, &inputCount); int outputCount; glGetProgramInterfaceiv(mHandle, GL_PROGRAM_OUTPUT, GL_ACTIVE_RESOURCES, &outputCount); int uniformBlockCount; glGetProgramInterfaceiv(mHandle, GL_UNIFORM_BLOCK, GL_ACTIVE_RESOURCES, &uniformBlockCount); int uniformCount; glGetProgramInterfaceiv(mHandle, GL_UNIFORM, GL_ACTIVE_RESOURCES, &uniformCount); // Gather inputs auto GatherMathVars = [&](int count, GLenum resourceType, ShaderInfo::ThingKind resourceKind, std::vector& list) { for (int i = 0; i < count; ++i) { const GLenum query[] = { GL_NAME_LENGTH, GL_TYPE, GL_LOCATION, GL_TOP_LEVEL_ARRAY_SIZE }; GLint props[std::size(query)]; glGetProgramResourceiv(mHandle, resourceType, i, std::size(query), query, std::size(props), nullptr, props); auto& nameLength = props[0]; auto& type = props[1]; auto& loc = props[2]; auto& arrayLength = props[3]; std::string fieldName(nameLength - 1, '\0'); glGetProgramResourceName(mHandle, GL_UNIFORM, i, nameLength, nullptr, fieldName.data()); mInfo->things.insert(fieldName, ThingId{ resourceKind, i }); auto& thing = list.emplace_back(ShaderInfo::InputOutputThing{}); auto& var = thing.variable; var.name = std::move(fieldName); var.arrayLength = arrayLength; QueryMathInfo(type, var.scalarType, var.width, var.height); } }; GatherMathVars(inputCount, GL_PROGRAM_INPUT, ShaderInfo::TKD_Input, mInfo->inputs); GatherMathVars(outputCount, GL_PROGRAM_OUTPUT, ShaderInfo::TKD_Output, mInfo->outputs); // Gather uniform variables for (int i = 0; i < uniformCount; ++i) { const GLenum query[] = { GL_BLOCK_INDEX, GL_NAME_LENGTH, GL_TYPE, GL_LOCATION, GL_TOP_LEVEL_ARRAY_SIZE }; GLint props[std::size(query)]; glGetProgramResourceiv(mHandle, GL_UNIFORM, i, std::size(query), query, std::size(props), nullptr, props); auto& blockIndex = props[0]; // Index in interface block if (blockIndex != -1) { // If this is an interface block uniform, skip because it will be handled by our uniform blocks inspector continue; } auto& nameLength = props[1]; auto& type = props[2]; auto& loc = props[3]; auto& arrayLength = props[4]; std::string fieldName(nameLength - 1, '\0'); glGetProgramResourceName(mHandle, GL_UNIFORM, i, nameLength, nullptr, fieldName.data()); mInfo->things.insert(fieldName, ThingId{ ShaderInfo::TKD_Uniform, i }); mInfo->uniforms.push_back(CreateVariable(type, loc)); } mInfo->things.shrink_to_fit(); #endif return true; } ShaderInfo* Shader::GetInfo() const { return mInfo.get(); } const std::string& Shader::GetName() const { return mName; } GLuint Shader::GetProgram() const { return mHandle; } bool Shader::IsValid() const { return mHandle != 0; } void ShaderManager::DiscoverShaders() { mShaders.clear(); auto path = AppConfig::assetDirPath / "Shaders"; if (!fs::exists(path)) { return; } for (auto& item : fs::directory_iterator(path)) { if (item.is_regular_file() && item.path().extension() == ".glsl") { auto shaderFile = Utils::OpenCstdioFile(item.path(), Utils::Read); if (!shaderFile) continue; DEFER { fclose(shaderFile); }; fseek(shaderFile, 0, SEEK_END); auto shaderFileSize = ftell(shaderFile); rewind(shaderFile); // Also add \0 ourselves auto buffer = std::make_unique(shaderFileSize + 1); fread(buffer.get(), shaderFileSize, 1, shaderFile); buffer[shaderFileSize] = '\0'; std::string_view source(buffer.get(), shaderFileSize); RcPtr shader(new Shader(item.path().stem().string())); std::string_view shaderName(shader->GetName()); // Load shader auto err = shader->InitFromSource(source); if (err != Shader::Success) { continue; } // Load shader info if present fs::path infoPath(item.path()); infoPath.replace_extension(".json"); if (fs::exists(infoPath)) { shader->CreateEmptyInfoIfAbsent(); shader->GetInfo()->LoadFromFile(infoPath); } mShaders.try_emplace(shaderName, std::move(shader)); } } } Shader* ShaderManager::FindShader(std::string_view name) { auto iter = mShaders.find(name); if (iter != mShaders.end()) { return iter->second.Get(); } else { return nullptr; } }