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>
This commit is contained in:
parent
ddaab5a32a
commit
51d87a990f
6 changed files with 229 additions and 1 deletions
|
|
@ -8,7 +8,10 @@ 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.
|
||||
// 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)) {
|
||||
|
|
@ -52,5 +55,75 @@ int main() {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue