Crafter.Graphics has to ship each of its compute shaders in two builds
of one source: one that indexes VK_EXT_descriptor_heap and one that
reads the same slots from a descriptor set, for drivers without the
extension. A Shader was a path, an entry point and a stage, and its
output name was derived from the path alone, so one source could only
be compiled once.
Shader gains `defines`, spelled like the compiler's -D ("NAME" is
defined to 1, "NAME=VALUE" to VALUE) and handed to glslang as a
preamble, and `variant`, which names the output: "ui.comp.glsl" with
variant "sets" becomes "ui.comp.sets.spv". OutputName() is the one
place that name is formed; Check, Compile and the forwarding of a
dependency's shaders into the consumer's bin dir all use it.
Two fields make one mistake easy: defines set, variant forgotten. Both
entries then compiled in parallel into the same file, the build
reported success with whichever finished last, and Check() called both
up to date from then on. The same has always been true of a.vert next
to a.frag. Build() now refuses a configuration in which two entries
share an output name and says which.
A define without a name, or one spanning lines, and a variant with a
path separator are rejected by name. Left to glslang the first two
surface as "-1:<line>" with no file, and the last as a failed open
after the whole compile.
Not addressed: Check() still compares only the source's mtime, so a
change to the defines, or to an included file, does not recompile
(#35).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
163 lines
7.7 KiB
C++
163 lines
7.7 KiB
C++
// 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<const fs::path> 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<EShMessages>(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<std::int32_t>(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:<line>" 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<std::uint32_t> 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<const char*>(spirv.data()), spirv.size() * sizeof(std::uint32_t));
|
|
|
|
if (!spvLog.empty()) {
|
|
return fail("SPIR-V codegen reported issues", std::move(spvLog));
|
|
}
|
|
glslang::FinalizeProcess();
|
|
return {};
|
|
}
|
|
}
|