Crafter.Build/tests/ShaderCompile/main.cpp
Jorijn van der Graaf 653dd0bb9b
Some checks failed
Flatpak / flatpak (push) Failing after 1m12s
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 20:20:29 +02:00

146 lines
6.6 KiB
C++

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Compile a tiny GLSL fragment shader directly via the Shader API. Verifies
// the SPIR-V output lands in the target dir and starts with the SPIR-V magic
// number, without dragging in the full Build() pipeline. The second half
// registers one source twice, with and without defines, and checks that the
// variant gets its own output, its own staleness and the other #ifdef branch
// (tests/ShaderVariantBuild drives the same thing through Build()).
int main() {
fs::path src = fs::current_path() / "tests" / "ShaderCompile" / "fixture" / "triangle.frag";
if (!fs::exists(src)) {
std::println(std::cerr, "fixture missing: {}", src.string());
return 1;
}
fs::path outDir = fs::temp_directory_path() / "crafter-build-shader-test";
std::error_code ec;
fs::remove_all(outDir, ec);
fs::create_directories(outDir);
Shader shader(fs::path(src), "main", ShaderType::Fragment);
std::array<fs::path, 0> includeDirs = {};
std::string err = shader.Compile(outDir, outDir, includeDirs);
if (!err.empty()) {
std::println(std::cerr, "shader compile failed: {}", err);
return 1;
}
fs::path spv = outDir / "triangle.spv";
if (!fs::exists(spv)) {
std::println(std::cerr, "spv not produced at {}", spv.string());
return 1;
}
// SPIR-V binaries start with the magic number 0x07230203 (little-endian
// when written by glslang on x86). Read the first four bytes to confirm
// the produced file is real SPIR-V, not a fluke empty file.
std::ifstream in(spv, std::ios::binary);
std::uint32_t magic = 0;
in.read(reinterpret_cast<char*>(&magic), sizeof(magic));
if (!in || magic != 0x07230203u) {
std::println(std::cerr, "spv has wrong magic: 0x{:08x}", magic);
return 1;
}
// Compiling again should be a no-op since the source isn't newer.
if (!shader.Check(outDir, outDir, includeDirs)) {
std::println(std::cerr, "Shader::Check returned false on an up-to-date spv");
return 1;
}
// Searched elsewhere, an #include could find another file: stale.
const std::array<fs::path, 1> elsewhere = { outDir / "elsewhere" };
if (shader.Check(outDir, outDir, elsewhere)) {
std::println(std::cerr, "Shader::Check called an spv up to date for other include directories");
return 1;
}
// So could a source of the same name moved to another directory, which
// its #includes are searched from first.
const fs::path movedDir = outDir / "moved";
fs::create_directories(movedDir);
fs::copy_file(src, movedDir / src.filename());
fs::last_write_time(movedDir / src.filename(), fs::last_write_time(src));
Shader moved(movedDir / src.filename(), "main", ShaderType::Fragment);
if (moved.Check(outDir, outDir, includeDirs)) {
std::println(std::cerr, "Shader::Check called an spv up to date for a source of the same name elsewhere");
return 1;
}
// One source registered twice: the second entry carries defines and a
// variant name. It must land in its own file instead of overwriting the
// first, and it must take the other #ifdef branch. Crafter.Graphics ships
// each UI shader in a descriptor-heap and a descriptor-set flavour this
// way; if the variant were dropped from the output name, whichever compile
// finished last would silently win.
fs::path variantSrc = fs::current_path() / "tests" / "ShaderCompile" / "fixture" / "variant.frag";
Shader plain(fs::path(variantSrc), "main", ShaderType::Fragment);
Shader red(fs::path(variantSrc), "main", ShaderType::Fragment);
red.defines = {"VARIANT_RED", "VARIANT_ALPHA=0.5"};
red.variant = "red";
if (plain.OutputName() != "variant.spv" || red.OutputName() != "variant.red.spv") {
std::println(std::cerr, "unexpected output names: {} and {}", plain.OutputName().string(), red.OutputName().string());
return 1;
}
for (const Shader* each : {&plain, &red}) {
// Staleness is per output, not per source: while only variant.spv
// exists, the variant is still due. A Check() that ignored the
// variant would call it up to date here, and Build() would then
// never compile a variant added to an already built project.
if (each == &red && red.Check(outDir, outDir, includeDirs)) {
std::println(std::cerr, "Shader::Check called the variant up to date before it was ever compiled");
return 1;
}
std::string variantErr = each->Compile(outDir, outDir, includeDirs);
if (!variantErr.empty()) {
std::println(std::cerr, "variant shader compile failed: {}", variantErr);
return 1;
}
if (!each->Check(outDir, outDir, includeDirs)) {
std::println(std::cerr, "Shader::Check returned false for {}", each->OutputName().string());
return 1;
}
}
// Malformed defines and variants are refused by name. Left to glslang, an
// empty macro name surfaces as "-1:<line>" with no file, and a variant
// with a separator as a failed open after the whole compile.
for (const char* bad : {"", "=1", "A\nB"}) {
Shader malformed(fs::path(variantSrc), "main", ShaderType::Fragment);
malformed.defines = {bad};
malformed.variant = "malformed";
if (!malformed.Compile(outDir, outDir, includeDirs).contains("invalid shader define")) {
std::println(std::cerr, "a malformed define was not rejected by name");
return 1;
}
}
Shader nested(fs::path(variantSrc), "main", ShaderType::Fragment);
nested.variant = "sub/red";
if (!nested.Compile(outDir, outDir, includeDirs).contains("path separator")) {
std::println(std::cerr, "a variant with a path separator was not rejected by name");
return 1;
}
auto readAll = [](const fs::path& file) {
std::ifstream stream(file, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
};
std::string plainSpv = readAll(outDir / "variant.spv");
std::string redSpv = readAll(outDir / "variant.red.spv");
if (plainSpv.empty() || redSpv.empty()) {
std::println(std::cerr, "a variant output is missing or empty");
return 1;
}
if (plainSpv == redSpv) {
std::println(std::cerr, "both variants compiled to the same SPIR-V: the defines did not reach the preprocessor");
return 1;
}
return 0;
}