Crafter.Build/tests/ShaderVariantBuild/main.cpp

121 lines
5.1 KiB
C++
Raw Permalink Normal View History

// 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;
// One shader source can be registered more than once, each entry with its own
// defines and a variant that names its output ("tint.spv", "tint.red.spv").
// Build() compiles the entries in parallel into one directory, which is where
// the feature can go wrong in ways the Shader API alone does not show:
//
// - both entries must land in the bin dir, with different contents;
// - an entry added later must still be compiled when the other one's output
// is already there and up to date;
// - two entries that would write the same file (defines set, variant
// forgotten) used to race silently: the file held whichever compile
// finished last, the build reported success, and Check() called both up
// to date from then on. That has to be a build error naming the cause.
//
// The fixture's `#if TINT_RED` also pins that a bare define is defined to 1,
// like the compiler's -D: an empty definition would not compile.
namespace {
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++Failures;
}
}
// Shader outputs are compared by mtime against the source, and the test
// adds an entry between passes, so work on a fresh copy outside the repo.
fs::path StageFixture() {
fs::path source = fs::current_path() / "tests" / "ShaderVariantBuild" / "fixture";
fs::path staged = fs::temp_directory_path() / "crafter-build-shader-variant-build";
fs::remove_all(staged);
fs::copy(source, staged, fs::copy_options::recursive);
return staged;
}
Configuration MakeApp(const fs::path& staged) {
Configuration app;
app.path = staged;
app.name = "tinted";
app.outputName = "tinted";
app.target = HostTarget();
app.type = ConfigurationType::Executable;
std::array<fs::path, 0> ifaces = {};
std::array<fs::path, 1> impls = { "main" };
app.GetInterfacesAndImplementations(ifaces, impls);
return app;
}
Shader& AddTint(Configuration& app, const fs::path& staged) {
return app.shaders.emplace_back(staged / "tint.frag", std::string("main"), ShaderType::Fragment);
}
BuildResult BuildOnce(Configuration& app) {
// A fresh depResults per pass: the map memoizes a Configuration's
// build for the duration of one pass.
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
std::mutex depMutex;
return Build(app, depResults, depMutex);
}
std::string ReadAll(const fs::path& file) {
std::ifstream stream(file, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
}
}
int main() {
const fs::path staged = StageFixture();
// Pass 1: the plain entry alone.
Configuration app = MakeApp(staged);
AddTint(app, staged);
BuildResult first = BuildOnce(app);
Check(first.result.empty(), std::format("the plain entry builds: {}", first.result));
const fs::path plainSpv = app.BinDir() / "tint.spv";
const fs::path redSpv = app.BinDir() / "tint.red.spv";
Check(fs::exists(plainSpv), "tint.spv is in the bin dir");
Check(!fs::exists(redSpv), "no variant output before the variant is registered");
// Pass 2: the variant is added while tint.spv is already up to date. It
// has its own staleness check and must be compiled, into its own file.
Shader& red = AddTint(app, staged);
red.defines = {"TINT_RED"};
red.variant = "red";
BuildResult second = BuildOnce(app);
Check(second.result.empty(), std::format("plain + variant builds: {}", second.result));
Check(fs::exists(redSpv), "tint.red.spv is in the bin dir");
const std::string plainBytes = ReadAll(plainSpv);
const std::string redBytes = ReadAll(redSpv);
Check(!plainBytes.empty() && !redBytes.empty(), "both outputs have content");
Check(plainBytes != redBytes, "the variant took the other #if branch");
// Pass 3: the same define without a variant. Both entries would write
// tint.spv, so the build has to stop and say so, and must leave the file
// from pass 2 alone.
Configuration clash = MakeApp(staged);
AddTint(clash, staged);
Shader& unnamed = AddTint(clash, staged);
unnamed.defines = {"TINT_RED"};
BuildResult third = BuildOnce(clash);
Check(!third.result.empty(), "two entries with one output name fail the build");
Check(third.result.contains("tint.spv") && third.result.contains("variant"), std::format("the error names the file and the fix: {}", third.result));
Check(ReadAll(plainSpv) == plainBytes, "the existing output is not overwritten by the failed build");
std::error_code ec;
fs::remove_all(staged, ec);
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;
}