// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #include "SPIRV/GlslangToSpv.h" #include "glslang/Public/ShaderLang.h" #include "glslang/Public/ResourceLimits.h" #include "../lib/DirStackFileIncluder.h" module Crafter.Build:Shader_impl; import :Shader; import std; namespace fs = std::filesystem; namespace { EShLanguage ToEShLanguage(Crafter::ShaderType t) { switch (t) { case Crafter::ShaderType::Vertex: return EShLangVertex; case Crafter::ShaderType::TessControl: return EShLangTessControl; case Crafter::ShaderType::TessEvaluation: return EShLangTessEvaluation; case Crafter::ShaderType::Geometry: return EShLangGeometry; case Crafter::ShaderType::Fragment: return EShLangFragment; case Crafter::ShaderType::Compute: return EShLangCompute; case Crafter::ShaderType::RayGen: return EShLangRayGen; case Crafter::ShaderType::Intersect: return EShLangIntersect; case Crafter::ShaderType::AnyHit: return EShLangAnyHit; case Crafter::ShaderType::ClosestHit: return EShLangClosestHit; case Crafter::ShaderType::Miss: return EShLangMiss; case Crafter::ShaderType::Callable: return EShLangCallable; case Crafter::ShaderType::Task: return EShLangTask; case Crafter::ShaderType::Mesh: return EShLangMesh; } return EShLangVertex; } } namespace Crafter { Shader::Shader(fs::path&& path, std::string&& entrypoint, ShaderType type) : path(std::move(path)), entrypoint(std::move(entrypoint)), type(type) { } fs::path Shader::OutputName() const { // replace_extension swaps only the last extension, so a stage suffix // survives: "ui.comp.glsl" becomes "ui.comp.spv", or "ui.comp.sets.spv" // for the variant "sets". fs::path name = path.filename(); name.replace_extension(variant.empty() ? std::string("spv") : std::format("{}.spv", variant)); return name; } bool Shader::Check(const fs::path& outputDir) const { fs::path spv = outputDir / OutputName(); return fs::exists(spv) && fs::last_write_time(path) < fs::last_write_time(spv); } std::string Shader::Compile(const fs::path& outputDir, std::span includeDirs) const { EShLanguage glslangType = ToEShLanguage(type); glslang::InitializeProcess(); // Every error path returns a non-empty string; the caller treats // empty as success (see Crafter.Build-Clang.cpp BuildOnce shader // worker). Prefixing with the source path is what tells the user // which shader actually failed when several compile in parallel. auto fail = [&](std::string_view stage, std::string log) { glslang::FinalizeProcess(); std::string out = path.string(); // Several variants of one source compile in parallel; without the // variant the message would not say which of them failed. if (!variant.empty()) out += std::format(" ({})", variant); out += ": "; out += stage; if (!log.empty()) { out += '\n'; out += log; } return out; }; constexpr EShMessages Messages = static_cast(EShMsgDefault | EShMsgVulkanRules | EShMsgSpvRules); std::ifstream fileStream(path, std::ios::in | std::ios::binary); if (!fileStream) { return fail("failed to open shader source", {}); } std::ostringstream contents; contents << fileStream.rdbuf(); std::string src = contents.str(); std::string pathStr = path.string(); const char* fileNameList[1] = { pathStr.c_str() }; const char* shaderSource = src.data(); const std::int32_t shaderSourceLen = static_cast(src.size()); // A variant is one component of a file name. Caught here, because a // separator would otherwise surface as "failed to open SPIR-V output" // after the whole compile. if (variant.find_first_of("/\\") != std::string::npos) { return fail(std::format("shader variant '{}' must not contain a path separator", variant), {}); } // One #define line per entry, split like the compiler's -D: "NAME" // defines it to 1, "NAME=VALUE" to VALUE. glslang places the preamble // ahead of the whole source (it finds #version by scanning first), so // line numbers in the user's file are unaffected. It keeps the // pointer it is handed instead of copying, so the string is declared // here to outlive parse() below. std::string preamble; for (const std::string& define : defines) { const std::size_t eq = define.find('='); const std::string name = define.substr(0, eq); // An error inside the preamble is reported as "-1:" with no // file and no macro name, so reject the malformed ones by name. if (name.empty() || define.find_first_of("\r\n") != std::string::npos) { return fail(std::format("invalid shader define '{}': expected NAME or NAME=VALUE on one line", define), {}); } preamble += std::format("#define {} {}\n", name, eq == std::string::npos ? std::string("1") : define.substr(eq + 1)); } glslang::TShader shader(glslangType); shader.setStringsWithLengthsAndNames(&shaderSource, &shaderSourceLen, fileNameList, 1); if (!preamble.empty()) shader.setPreamble(preamble.c_str()); shader.setEntryPoint(entrypoint.c_str()); shader.setSourceEntryPoint(entrypoint.c_str()); shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_4); DirStackFileIncluder includeDir; includeDir.pushExternalLocalDirectory(path.parent_path().generic_string()); for (const fs::path& dir : includeDirs) { includeDir.pushExternalLocalDirectory(dir.generic_string()); } if (!shader.parse(GetDefaultResources(), 100, false, Messages, includeDir)) { return fail("GLSL parse failed", std::string(shader.getInfoLog()) + shader.getInfoDebugLog()); } glslang::TProgram program; program.addShader(&shader); if (!program.link(Messages)) { return fail("GLSL link failed", std::string(program.getInfoLog()) + program.getInfoDebugLog()); } glslang::TIntermediate* intermediate = program.getIntermediate(glslangType); if (!intermediate) { // Defensive: parse+link succeeded above, so this should be // unreachable. If glslang ever changes that contract we'd // rather surface a clear error than dereference null (the // pre-fix bug that masqueraded as a silent SIGSEGV). return fail("glslang produced no intermediate code", {}); } spv::SpvBuildLogger logger; std::vector spirv; glslang::GlslangToSpv(*intermediate, spirv, &logger); std::string spvLog = logger.getAllMessages(); fs::path filename = OutputName(); std::ofstream file(outputDir/filename, std::ios::binary); if (!file) { return fail("failed to open SPIR-V output", (outputDir/filename).string()); } file.write(reinterpret_cast(spirv.data()), spirv.size() * sizeof(std::uint32_t)); if (!spvLog.empty()) { return fail("SPIR-V codegen reported issues", std::move(spvLog)); } glslang::FinalizeProcess(); return {}; } }