/* Copyright (c) 2017-2026 Hans-Kristian Arntzen * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "compiler.hpp" #include "shaderc/shaderc.hpp" #include "path_utils.hpp" #include "logging.hpp" #include "string_helpers.hpp" #include "spirv-tools/libspirv.hpp" namespace Granite { GLSLCompiler::GLSLCompiler(FilesystemInterface &iface_) : iface(iface_) { } static Stage stage_from_path(const std::string &path) { auto ext = Path::ext(path); if (ext == "vert") return Stage::Vertex; else if (ext == "frag") return Stage::Fragment; else if (ext == "comp") return Stage::Compute; else if (ext == "task") return Stage::Task; else if (ext == "mesh") return Stage::Mesh; else return Stage::Unknown; } static Stage convert_stage(const std::string &stage) { if (stage == "vertex") return Stage::Vertex; else if (stage == "compute") return Stage::Compute; else if (stage == "fragment") return Stage::Fragment; else if (stage == "task") return Stage::Task; else if (stage == "mesh") return Stage::Mesh; else return Stage::Unknown; } bool GLSLCompiler::set_source_from_file(const std::string &path, Stage forced_stage) { if (!iface.load_text_file(path, source)) { LOGE("Failed to load shader: %s\n", path.c_str()); return false; } source_path = path; if (forced_stage != Stage::Unknown) stage = forced_stage; else stage = stage_from_path(path); return stage != Stage::Unknown; } bool GLSLCompiler::set_source_from_file_multistage(const std::string &path) { if (iface.load_text_file(path, source)) { LOGE("Failed to load shader: %s\n", path.c_str()); return false; } source_path = path; stage = Stage::Unknown; return true; } void GLSLCompiler::set_include_directories(const std::vector *include_directories_) { include_directories = include_directories_; } bool GLSLCompiler::find_include_path(const std::string &source_path_, const std::string &include_path, std::string &included_path, std::string &included_source) { auto relpath = Path::relpath(source_path_, include_path); if (iface.load_text_file(relpath, included_source)) { included_path = relpath; return true; } if (include_directories) { for (auto &include_dir : *include_directories) { auto path = Path::join(include_dir, include_path); if (iface.load_text_file(path, included_source)) { included_path = path; return true; } } } return false; } bool GLSLCompiler::parse_variants(const std::string &source_, const std::string &path) { auto lines = Util::split(source_, "\n"); unsigned line_index = 1; size_t offset; for (auto &line : lines) { // This check, followed by the include statement check below isn't even remotely correct, // but we only have to care about shaders we control here. if ((offset = line.find("//")) != std::string::npos) line = line.substr(0, offset); if ((offset = line.find("#include \"")) != std::string::npos) { auto include_path = line.substr(offset + 10); if (!include_path.empty() && include_path.back() == '"') include_path.pop_back(); std::string included_source; if (!find_include_path(path, include_path, include_path, included_source)) { LOGE("Failed to include GLSL file: %s\n", include_path.c_str()); return false; } preprocessed_source += Util::join("#line ", 1, " \"", include_path, "\"\n"); if (!parse_variants(included_source, include_path)) return false; preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n"); dependencies.insert(include_path); } else if (line.find("#pragma optimize off") == 0) { optimization = Optimization::ForceOff; preprocessed_source += "// #pragma optimize off"; preprocessed_source += '\n'; } else if (line.find("#pragma optimize on") == 0) { optimization = Optimization::ForceOn; preprocessed_source += "// #pragma optimize on"; preprocessed_source += '\n'; } else if (line.find("#pragma stage ") == 0) { if (!preprocessed_source.empty()) { preprocessed_sections.push_back({ preprocessing_active_stage, std::move(preprocessed_source) }); preprocessed_source = {}; } preprocessing_active_stage = convert_stage(line.substr(14)); preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n"); } else if (line.find("#pragma ") == 0) { pragmas.push_back(line.substr(8)); preprocessed_source += "// "; preprocessed_source += line; preprocessed_source += '\n'; } else { preprocessed_source += line; preprocessed_source += '\n'; auto first_non_space = line.find_first_not_of(' '); if (first_non_space != std::string::npos && line[first_non_space] == '#') { auto keywords = Util::split(line.substr(first_non_space + 1), " "); if (keywords.size() == 1) { auto &word = keywords.front(); if (word == "endif") preprocessed_source += Util::join("#line ", line_index + 1, " \"", path, "\"\n"); } } } line_index++; } return true; } bool GLSLCompiler::preprocess() { // Use a custom preprocessor where we only resolve includes once. // The builtin glslang preprocessor is not suitable for this task, // since we need to defer resolving defines. preprocessed_source.clear(); pragmas.clear(); preprocessed_sections.clear(); preprocessing_active_stage = Stage::Unknown; bool ret = parse_variants(source, source_path); if (ret && !preprocessed_source.empty()) { preprocessed_sections.push_back({ preprocessing_active_stage, std::move(preprocessed_source) }); preprocessed_source = {}; } return ret; } Util::Hash GLSLCompiler::get_source_hash() const { Util::Hasher h; for (auto §ion : preprocessed_sections) { h.u32(uint32_t(section.stage)); h.string(section.source); } h.string(preprocessed_source); return h.get(); } std::vector GLSLCompiler::compile(std::string &error_message, const std::vector> *defines) const { shaderc::Compiler compiler; shaderc::CompileOptions options; if (preprocessed_sections.empty()) { error_message = "Need to preprocess source first."; return {}; } if (defines) for (auto &define : *defines) options.AddMacroDefinition(define.first, std::to_string(define.second)); #if GRANITE_COMPILER_OPTIMIZE if (optimization != Optimization::ForceOff) options.SetOptimizationLevel(shaderc_optimization_level_performance); else options.SetOptimizationLevel(shaderc_optimization_level_zero); #else options.SetOptimizationLevel(shaderc_optimization_level_zero); #endif if (!strip && (GRANITE_COMPILER_DEBUG || optimization == Optimization::ForceOff)) options.SetGenerateDebugInfo(); shaderc_shader_kind kind; switch (stage) { case Stage::Vertex: kind = shaderc_glsl_vertex_shader; options.AddMacroDefinition("STAGE_VERTEX", "1"); break; case Stage::Fragment: kind = shaderc_glsl_fragment_shader; options.AddMacroDefinition("STAGE_FRAGMENT", "1"); break; case Stage::Compute: kind = shaderc_glsl_compute_shader; options.AddMacroDefinition("STAGE_COMPUTE", "1"); break; case Stage::Task: kind = shaderc_glsl_task_shader; options.AddMacroDefinition("STAGE_TASK", "1"); break; case Stage::Mesh: kind = shaderc_glsl_mesh_shader; options.AddMacroDefinition("STAGE_MESH", "1"); break; default: error_message = "Invalid shader stage."; return {}; } auto env = shaderc_env_version_vulkan_1_1; auto spv_version = shaderc_spirv_version_1_3; if (target == Target::Vulkan12) { env = shaderc_env_version_vulkan_1_2; spv_version = shaderc_spirv_version_1_5; } else if (target == Target::Vulkan13) { env = shaderc_env_version_vulkan_1_3; spv_version = shaderc_spirv_version_1_6; } options.SetTargetEnvironment(shaderc_target_env_vulkan, env); options.SetTargetSpirv(spv_version); options.SetSourceLanguage(shaderc_source_language_glsl); shaderc::SpvCompilationResult result; if (preprocessed_sections.size() == 1) { if (preprocessed_sections.front().stage != Stage::Unknown && preprocessed_sections.front().stage != stage) { error_message = "No preprocessed sections available."; return {}; } result = compiler.CompileGlslToSpv(preprocessed_sections.front().source, kind, source_path.c_str(), options); } else { std::string combined_source; for (auto §ion : preprocessed_sections) if (section.stage == Stage::Unknown || section.stage == stage) combined_source += section.source; if (combined_source.empty()) { error_message = "No preprocessed sections available."; return {}; } result = compiler.CompileGlslToSpv(combined_source, kind, source_path.c_str(), options); } error_message.clear(); if (result.GetCompilationStatus() != shaderc_compilation_status_success) { error_message = result.GetErrorMessage(); return {}; } std::vector compiled_spirv(result.cbegin(), result.cend()); #if 0 spvtools::SpirvTools core(target == Target::Vulkan13 ? SPV_ENV_VULKAN_1_3 : SPV_ENV_VULKAN_1_1); core.SetMessageConsumer([&error_message](spv_message_level_t, const char *, const spv_position_t&, const char *message) { error_message = message; }); spvtools::ValidatorOptions opts; opts.SetScalarBlockLayout(true); if (!core.Validate(compiled_spirv.data(), compiled_spirv.size(), opts)) { error_message += "\nFailed to validate SPIR-V.\n"; return {}; } #endif return compiled_spirv; } }