2026-07-23 01:24:42 +02:00
|
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
2025-10-31 16:50:47 +01:00
|
|
|
|
|
|
|
|
module;
|
2026-03-02 21:11:56 +01:00
|
|
|
#include "SPIRV/GlslangToSpv.h"
|
|
|
|
|
#include "glslang/Public/ShaderLang.h"
|
|
|
|
|
#include "glslang/Public/ResourceLimits.h"
|
2025-10-31 16:50:47 +01:00
|
|
|
#include "../lib/DirStackFileIncluder.h"
|
|
|
|
|
module Crafter.Build:Shader_impl;
|
|
|
|
|
import :Shader;
|
fix: rebuild and relink on what changed, submodules at their revisions, and mingw found where it is
- Shaders record the files glslang opened (a depfile) and their settings
(source path, stage, entry point, defines, include directories) in the
build directory, and rebuild when an included file or a setting changes,
not only the shader's own source.
- An executable relinks when its link command changed, not only when an
object is newer: a link flag in project.cpp, or a dependency's library
that moved (an ExternalDependency built into a new cache directory). The
command is recorded beside the objects; its flags come from ordered sets,
so the order the dependency threads finish in can't change it.
- An external's newest artifact counts its libDirs and versioned .so names.
- Submodules are checked out at the revisions the fetched commit records,
from the URLs its .gitmodules gives, after every clone, switch, checkout
and pull, so a pinned commit builds against its own submodules, and one
moved by hand is put back; with no .gitmodules, git isn't asked.
- Each external clone's fetch and build hold a file lock: parallel builds
pulled into one clone at once ("cannot lock ref").
- mingw:
- -stdlib=libstdc++ always;
- the std module's source is found through libstdc++.modules.json in the
mingw toolchain's own library directories (the generic lib/ holds the
host's Linux one), asking the compiler once a run;
- a toolchain that isn't there is a build error saying where it looked,
not std::terminate;
- CRAFTER_MINGW_DIR names the prefix, the directory holding
x86_64-w64-mingw32/ and lib/gcc/;
- inside the Flatpak the host's /usr mingw is used.
- RunScript's exit-status check reads as the one range it is.
- Tests: ConcurrentExternalFetch, ExternalArtifactTime, GitSubmodules,
LinkFlagsChange, MingwMissing, ShaderIncludeChange; ShaderCompile and
ToolchainResolve extended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:08:13 +02:00
|
|
|
import :Platform;
|
2025-10-31 16:50:47 +01:00
|
|
|
import std;
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
2026-04-27 07:04:42 +02:00
|
|
|
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;
|
|
|
|
|
}
|
fix: rebuild and relink on what changed, submodules at their revisions, and mingw found where it is
- Shaders record the files glslang opened (a depfile) and their settings
(source path, stage, entry point, defines, include directories) in the
build directory, and rebuild when an included file or a setting changes,
not only the shader's own source.
- An executable relinks when its link command changed, not only when an
object is newer: a link flag in project.cpp, or a dependency's library
that moved (an ExternalDependency built into a new cache directory). The
command is recorded beside the objects; its flags come from ordered sets,
so the order the dependency threads finish in can't change it.
- An external's newest artifact counts its libDirs and versioned .so names.
- Submodules are checked out at the revisions the fetched commit records,
from the URLs its .gitmodules gives, after every clone, switch, checkout
and pull, so a pinned commit builds against its own submodules, and one
moved by hand is put back; with no .gitmodules, git isn't asked.
- Each external clone's fetch and build hold a file lock: parallel builds
pulled into one clone at once ("cannot lock ref").
- mingw:
- -stdlib=libstdc++ always;
- the std module's source is found through libstdc++.modules.json in the
mingw toolchain's own library directories (the generic lib/ holds the
host's Linux one), asking the compiler once a run;
- a toolchain that isn't there is a build error saying where it looked,
not std::terminate;
- CRAFTER_MINGW_DIR names the prefix, the directory holding
x86_64-w64-mingw32/ and lib/gcc/;
- inside the Flatpak the host's /usr mingw is used.
- RunScript's exit-status check reads as the one range it is.
- Tests: ConcurrentExternalFetch, ExternalArtifactTime, GitSubmodules,
LinkFlagsChange, MingwMissing, ShaderIncludeChange; ShaderCompile and
ToolchainResolve extended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:08:13 +02:00
|
|
|
|
|
|
|
|
// A path as a Make prerequisite, escaped the way NewestPrerequisite reads it.
|
|
|
|
|
std::string MakeEscaped(const std::string& path) {
|
|
|
|
|
std::string escaped;
|
|
|
|
|
for (const char c : path) {
|
|
|
|
|
if (c == ' ' || c == '\t' || c == '#') escaped += '\\';
|
|
|
|
|
if (c == '$') escaped += '$';
|
|
|
|
|
escaped += c;
|
|
|
|
|
}
|
|
|
|
|
return escaped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string ReadFile(const fs::path& file) {
|
|
|
|
|
std::ifstream stream(file, std::ios::binary);
|
|
|
|
|
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// What a compile's output depends on besides files' contents, one line
|
|
|
|
|
// each: two entries of one source differ here, and an #include searched
|
|
|
|
|
// for from another source directory or in other directories can find
|
|
|
|
|
// another file.
|
|
|
|
|
std::string Settings(const Crafter::Shader& shader, std::span<const fs::path> includeDirs) {
|
|
|
|
|
std::string settings = std::format("source {}\nstage {}\nentry {}\n", fs::absolute(shader.path).string(), static_cast<int>(shader.type), shader.entrypoint);
|
|
|
|
|
for (const std::string& define : shader.defines) settings += std::format("define {}\n", define);
|
|
|
|
|
for (const fs::path& dir : includeDirs) settings += std::format("include {}\n", dir.string());
|
|
|
|
|
return settings;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Where Compile records, for Check, what the output in the bin dir was
|
|
|
|
|
// built from: beside the configuration's objects, as their depfiles are.
|
|
|
|
|
fs::path Record(const Crafter::Shader& shader, const fs::path& buildDir, std::string_view kind) {
|
|
|
|
|
return buildDir / std::format("{}.{}", shader.OutputName().string(), kind);
|
|
|
|
|
}
|
2026-04-27 07:04:42 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-31 16:50:47 +01:00
|
|
|
namespace Crafter {
|
2026-04-27 07:04:42 +02:00
|
|
|
Shader::Shader(fs::path&& path, std::string&& entrypoint, ShaderType type) : path(std::move(path)), entrypoint(std::move(entrypoint)), type(type) {
|
2025-10-31 16:50:47 +01:00
|
|
|
|
feat(shader): per-entry defines and a named output variant
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>
2026-10-06 16:50:41 +02:00
|
|
|
}
|
|
|
|
|
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;
|
2025-10-31 16:50:47 +01:00
|
|
|
}
|
fix: rebuild and relink on what changed, submodules at their revisions, and mingw found where it is
- Shaders record the files glslang opened (a depfile) and their settings
(source path, stage, entry point, defines, include directories) in the
build directory, and rebuild when an included file or a setting changes,
not only the shader's own source.
- An executable relinks when its link command changed, not only when an
object is newer: a link flag in project.cpp, or a dependency's library
that moved (an ExternalDependency built into a new cache directory). The
command is recorded beside the objects; its flags come from ordered sets,
so the order the dependency threads finish in can't change it.
- An external's newest artifact counts its libDirs and versioned .so names.
- Submodules are checked out at the revisions the fetched commit records,
from the URLs its .gitmodules gives, after every clone, switch, checkout
and pull, so a pinned commit builds against its own submodules, and one
moved by hand is put back; with no .gitmodules, git isn't asked.
- Each external clone's fetch and build hold a file lock: parallel builds
pulled into one clone at once ("cannot lock ref").
- mingw:
- -stdlib=libstdc++ always;
- the std module's source is found through libstdc++.modules.json in the
mingw toolchain's own library directories (the generic lib/ holds the
host's Linux one), asking the compiler once a run;
- a toolchain that isn't there is a build error saying where it looked,
not std::terminate;
- CRAFTER_MINGW_DIR names the prefix, the directory holding
x86_64-w64-mingw32/ and lib/gcc/;
- inside the Flatpak the host's /usr mingw is used.
- RunScript's exit-status check reads as the one range it is.
- Tests: ConcurrentExternalFetch, ExternalArtifactTime, GitSubmodules,
LinkFlagsChange, MingwMissing, ShaderIncludeChange; ShaderCompile and
ToolchainResolve extended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:08:13 +02:00
|
|
|
bool Shader::Check(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const {
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
const fs::file_time_type built = fs::last_write_time(outputDir / OutputName(), ec);
|
|
|
|
|
if (ec) return false;
|
|
|
|
|
// The source and every file it #included, as the last compile
|
|
|
|
|
// recorded them: without a record, or with an included file since
|
|
|
|
|
// deleted, NewestPrerequisite reads max(), which is stale.
|
|
|
|
|
if (std::max(fs::last_write_time(path), NewestPrerequisite(Record(*this, buildDir, "d"))) >= built) return false;
|
|
|
|
|
return ReadFile(Record(*this, buildDir, "settings")) == Settings(*this, includeDirs);
|
2025-10-31 16:50:47 +01:00
|
|
|
}
|
fix: rebuild and relink on what changed, submodules at their revisions, and mingw found where it is
- Shaders record the files glslang opened (a depfile) and their settings
(source path, stage, entry point, defines, include directories) in the
build directory, and rebuild when an included file or a setting changes,
not only the shader's own source.
- An executable relinks when its link command changed, not only when an
object is newer: a link flag in project.cpp, or a dependency's library
that moved (an ExternalDependency built into a new cache directory). The
command is recorded beside the objects; its flags come from ordered sets,
so the order the dependency threads finish in can't change it.
- An external's newest artifact counts its libDirs and versioned .so names.
- Submodules are checked out at the revisions the fetched commit records,
from the URLs its .gitmodules gives, after every clone, switch, checkout
and pull, so a pinned commit builds against its own submodules, and one
moved by hand is put back; with no .gitmodules, git isn't asked.
- Each external clone's fetch and build hold a file lock: parallel builds
pulled into one clone at once ("cannot lock ref").
- mingw:
- -stdlib=libstdc++ always;
- the std module's source is found through libstdc++.modules.json in the
mingw toolchain's own library directories (the generic lib/ holds the
host's Linux one), asking the compiler once a run;
- a toolchain that isn't there is a build error saying where it looked,
not std::terminate;
- CRAFTER_MINGW_DIR names the prefix, the directory holding
x86_64-w64-mingw32/ and lib/gcc/;
- inside the Flatpak the host's /usr mingw is used.
- RunScript's exit-status check reads as the one range it is.
- Tests: ConcurrentExternalFetch, ExternalArtifactTime, GitSubmodules,
LinkFlagsChange, MingwMissing, ShaderIncludeChange; ShaderCompile and
ToolchainResolve extended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:08:13 +02:00
|
|
|
std::string Shader::Compile(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const {
|
|
|
|
|
// The records go first, so a compile that fails leaves the output
|
|
|
|
|
// stale, whatever it wrote.
|
|
|
|
|
const fs::path depFile = Record(*this, buildDir, "d");
|
|
|
|
|
const fs::path settingsFile = Record(*this, buildDir, "settings");
|
|
|
|
|
std::error_code removed;
|
|
|
|
|
fs::remove(depFile, removed);
|
|
|
|
|
fs::remove(settingsFile, removed);
|
|
|
|
|
|
2026-04-27 07:04:42 +02:00
|
|
|
EShLanguage glslangType = ToEShLanguage(type);
|
2025-10-31 16:50:47 +01:00
|
|
|
glslang::InitializeProcess();
|
2026-05-01 19:02:14 +02:00
|
|
|
|
|
|
|
|
// 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();
|
feat(shader): per-entry defines and a named output variant
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>
2026-10-06 16:50:41 +02:00
|
|
|
// 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);
|
2026-05-01 19:02:14 +02:00
|
|
|
out += ": ";
|
|
|
|
|
out += stage;
|
|
|
|
|
if (!log.empty()) {
|
|
|
|
|
out += '\n';
|
|
|
|
|
out += log;
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-31 02:05:37 +02:00
|
|
|
constexpr EShMessages Messages = static_cast<EShMessages>(EShMsgDefault | EShMsgVulkanRules | EShMsgSpvRules);
|
2025-10-31 16:50:47 +01:00
|
|
|
std::ifstream fileStream(path, std::ios::in | std::ios::binary);
|
|
|
|
|
if (!fileStream) {
|
2026-05-01 19:02:14 +02:00
|
|
|
return fail("failed to open shader source", {});
|
2025-10-31 16:50:47 +01:00
|
|
|
}
|
|
|
|
|
std::ostringstream contents;
|
|
|
|
|
contents << fileStream.rdbuf();
|
|
|
|
|
std::string src = contents.str();
|
2026-04-27 07:04:42 +02:00
|
|
|
|
2026-05-01 19:02:14 +02:00
|
|
|
std::string pathStr = path.string();
|
feat(lint): naming reads the AST, deleting the scope heuristic
The rule was 125 lines: four std::regex, a hand-rolled {/} scope stack, a
cumulative paren-depth counter so a wrapped parameter list would not look like
a declaration, a 40-entry keyword denylist, and a "function-shaped line" guess
whose own comment conceded it was heuristic. Storage class came from
lineStr.contains("static "). It is now ~55 lines that ask clang what kind of
declaration each thing is and what encloses it.
The three regressions the old version carried special cases for — a call with
an inline lambda argument, a bare statement call, a one-liner method — need no
handling at all, because a call is not a declaration. Their tests pass
unchanged.
On this repository the exact version found 42 violations the heuristic had
never been able to see, all real:
- 37 members of the libclang function-pointer table added two commits ago
were PascalCase. The old varDecl regex could not match a declaration whose
type is decltype(&f), so they were silently skipped. Renamed to camelCase,
which mirrors clang_createIndex -> createIndex more closely anyway.
- Crafter.Build-Shader.cpp had a snake_case local, file_name_list, invisible
to the heuristic for the same reason (it declares a const char* array).
- Four extern "C" declarations of libc functions in tests were reported as
badly-named functions. Those are named by the C library, so C language
linkage is now an exemption — the same principled test no-char-pointer
uses, rather than another denylist entry.
New tests cover what the line-based version structurally could not reach: a
signature wrapped over several lines, `static` on its own line above the
declaration it applies to, and a member versus a local inside a method.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:26 +02:00
|
|
|
const char* fileNameList[1] = { pathStr.c_str() };
|
2026-07-23 01:24:42 +02:00
|
|
|
const char* shaderSource = src.data();
|
|
|
|
|
const std::int32_t shaderSourceLen = static_cast<std::int32_t>(src.size());
|
2026-04-27 07:04:42 +02:00
|
|
|
|
feat(shader): per-entry defines and a named output variant
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>
2026-10-06 16:50:41 +02:00
|
|
|
// 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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 07:04:42 +02:00
|
|
|
glslang::TShader shader(glslangType);
|
feat(lint): naming reads the AST, deleting the scope heuristic
The rule was 125 lines: four std::regex, a hand-rolled {/} scope stack, a
cumulative paren-depth counter so a wrapped parameter list would not look like
a declaration, a 40-entry keyword denylist, and a "function-shaped line" guess
whose own comment conceded it was heuristic. Storage class came from
lineStr.contains("static "). It is now ~55 lines that ask clang what kind of
declaration each thing is and what encloses it.
The three regressions the old version carried special cases for — a call with
an inline lambda argument, a bare statement call, a one-liner method — need no
handling at all, because a call is not a declaration. Their tests pass
unchanged.
On this repository the exact version found 42 violations the heuristic had
never been able to see, all real:
- 37 members of the libclang function-pointer table added two commits ago
were PascalCase. The old varDecl regex could not match a declaration whose
type is decltype(&f), so they were silently skipped. Renamed to camelCase,
which mirrors clang_createIndex -> createIndex more closely anyway.
- Crafter.Build-Shader.cpp had a snake_case local, file_name_list, invisible
to the heuristic for the same reason (it declares a const char* array).
- Four extern "C" declarations of libc functions in tests were reported as
badly-named functions. Those are named by the C library, so C language
linkage is now an exemption — the same principled test no-char-pointer
uses, rather than another denylist entry.
New tests cover what the line-based version structurally could not reach: a
signature wrapped over several lines, `static` on its own line above the
declaration it applies to, and a member versus a local inside a method.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:53:26 +02:00
|
|
|
shader.setStringsWithLengthsAndNames(&shaderSource, &shaderSourceLen, fileNameList, 1);
|
feat(shader): per-entry defines and a named output variant
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>
2026-10-06 16:50:41 +02:00
|
|
|
if (!preamble.empty()) shader.setPreamble(preamble.c_str());
|
2025-10-31 16:50:47 +01:00
|
|
|
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());
|
2026-05-02 21:08:51 +02:00
|
|
|
for (const fs::path& dir : includeDirs) {
|
|
|
|
|
includeDir.pushExternalLocalDirectory(dir.generic_string());
|
|
|
|
|
}
|
2026-04-27 07:04:42 +02:00
|
|
|
|
2026-07-31 02:05:37 +02:00
|
|
|
if (!shader.parse(GetDefaultResources(), 100, false, Messages, includeDir)) {
|
2026-05-01 19:02:14 +02:00
|
|
|
return fail("GLSL parse failed", std::string(shader.getInfoLog()) + shader.getInfoDebugLog());
|
2025-10-31 16:50:47 +01:00
|
|
|
}
|
2026-04-27 07:04:42 +02:00
|
|
|
|
2026-05-01 19:02:14 +02:00
|
|
|
glslang::TProgram program;
|
|
|
|
|
program.addShader(&shader);
|
2026-07-31 02:05:37 +02:00
|
|
|
if (!program.link(Messages)) {
|
2026-05-01 19:02:14 +02:00
|
|
|
return fail("GLSL link failed", std::string(program.getInfoLog()) + program.getInfoDebugLog());
|
2025-10-31 16:50:47 +01:00
|
|
|
}
|
2026-04-27 07:04:42 +02:00
|
|
|
|
|
|
|
|
glslang::TIntermediate* intermediate = program.getIntermediate(glslangType);
|
2026-05-01 19:02:14 +02:00
|
|
|
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", {});
|
2025-10-31 16:50:47 +01:00
|
|
|
}
|
2026-04-27 07:04:42 +02:00
|
|
|
|
2025-10-31 16:50:47 +01:00
|
|
|
spv::SpvBuildLogger logger;
|
|
|
|
|
std::vector<std::uint32_t> spirv;
|
|
|
|
|
glslang::GlslangToSpv(*intermediate, spirv, &logger);
|
2026-05-01 19:02:14 +02:00
|
|
|
std::string spvLog = logger.getAllMessages();
|
|
|
|
|
|
feat(shader): per-entry defines and a named output variant
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>
2026-10-06 16:50:41 +02:00
|
|
|
fs::path filename = OutputName();
|
2025-10-31 16:50:47 +01:00
|
|
|
std::ofstream file(outputDir/filename, std::ios::binary);
|
2026-05-01 19:02:14 +02:00
|
|
|
if (!file) {
|
|
|
|
|
return fail("failed to open SPIR-V output", (outputDir/filename).string());
|
|
|
|
|
}
|
2025-10-31 16:50:47 +01:00
|
|
|
file.write(reinterpret_cast<const char*>(spirv.data()), spirv.size() * sizeof(std::uint32_t));
|
2026-05-01 19:02:14 +02:00
|
|
|
|
|
|
|
|
if (!spvLog.empty()) {
|
|
|
|
|
return fail("SPIR-V codegen reported issues", std::move(spvLog));
|
|
|
|
|
}
|
fix: rebuild and relink on what changed, submodules at their revisions, and mingw found where it is
- Shaders record the files glslang opened (a depfile) and their settings
(source path, stage, entry point, defines, include directories) in the
build directory, and rebuild when an included file or a setting changes,
not only the shader's own source.
- An executable relinks when its link command changed, not only when an
object is newer: a link flag in project.cpp, or a dependency's library
that moved (an ExternalDependency built into a new cache directory). The
command is recorded beside the objects; its flags come from ordered sets,
so the order the dependency threads finish in can't change it.
- An external's newest artifact counts its libDirs and versioned .so names.
- Submodules are checked out at the revisions the fetched commit records,
from the URLs its .gitmodules gives, after every clone, switch, checkout
and pull, so a pinned commit builds against its own submodules, and one
moved by hand is put back; with no .gitmodules, git isn't asked.
- Each external clone's fetch and build hold a file lock: parallel builds
pulled into one clone at once ("cannot lock ref").
- mingw:
- -stdlib=libstdc++ always;
- the std module's source is found through libstdc++.modules.json in the
mingw toolchain's own library directories (the generic lib/ holds the
host's Linux one), asking the compiler once a run;
- a toolchain that isn't there is a build error saying where it looked,
not std::terminate;
- CRAFTER_MINGW_DIR names the prefix, the directory holding
x86_64-w64-mingw32/ and lib/gcc/;
- inside the Flatpak the host's /usr mingw is used.
- RunScript's exit-status check reads as the one range it is.
- Tests: ConcurrentExternalFetch, ExternalArtifactTime, GitSubmodules,
LinkFlagsChange, MingwMissing, ShaderIncludeChange; ShaderCompile and
ToolchainResolve extended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:08:13 +02:00
|
|
|
|
|
|
|
|
// What Check compares the output against next time: the source and
|
|
|
|
|
// the files it included, as absolute paths so that reading them back
|
|
|
|
|
// doesn't depend on the directory a build runs from, and the settings.
|
|
|
|
|
std::ofstream dep(depFile, std::ios::binary);
|
|
|
|
|
dep << MakeEscaped(fs::absolute(outputDir / filename).string()) << ": " << MakeEscaped(fs::absolute(path).string());
|
|
|
|
|
for (const std::string& included : includeDir.getIncludedFiles()) {
|
|
|
|
|
dep << " \\\n " << MakeEscaped(fs::absolute(included).string());
|
|
|
|
|
}
|
|
|
|
|
dep << '\n';
|
|
|
|
|
dep.close();
|
|
|
|
|
std::ofstream settings(settingsFile, std::ios::binary);
|
|
|
|
|
settings << Settings(*this, includeDirs);
|
|
|
|
|
settings.close();
|
|
|
|
|
if (!dep || !settings) {
|
|
|
|
|
fs::remove(depFile, removed);
|
|
|
|
|
fs::remove(settingsFile, removed);
|
|
|
|
|
return fail("failed to record what the shader was built from", buildDir.string());
|
|
|
|
|
}
|
2026-05-01 19:02:14 +02:00
|
|
|
glslang::FinalizeProcess();
|
|
|
|
|
return {};
|
2025-10-31 16:50:47 +01:00
|
|
|
}
|
|
|
|
|
}
|