Crafter.Build/tests/ShaderCompile/main.cpp
Jorijn van der Graaf 51d87a990f
test(shader): variants through the Shader API and through Build()
ShaderCompile registers one source twice and checks that the variant
gets its own output name, takes the other #ifdef branch and receives
the value of a NAME=VALUE define. Staleness is pinned per output: while
only the plain output exists the variant must still be due, or Build()
would never compile a variant added to a built project. Malformed
defines and a variant with a path separator must be refused by name.

ShaderVariantBuild drives the same through Build(), which is where the
entries compile in parallel into one directory: a variant added to an
already built configuration is compiled into its own file, and two
entries with one output name fail the build without touching the
existing output. Its fixture tests the macro with `#if`, which pins
that a bare define is 1 and not empty.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 16:50:41 +02:00

129 lines
5.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, 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)) {
std::println(std::cerr, "Shader::Check returned false on an up-to-date spv");
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)) {
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, includeDirs);
if (!variantErr.empty()) {
std::println(std::cerr, "variant shader compile failed: {}", variantErr);
return 1;
}
if (!each->Check(outDir)) {
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, 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, 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;
}