//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Every GLSL shader the examples ship still compiles. The examples are // projects of their own that nothing in this one builds, and crafter-build // compiles their shaders with whichever glslang it links, unpinned. When // glslang began rejecting a descriptor_heap combined image sampler (upstream // c8253623), examples/Sponza stopped building and nothing noticed until it // was built by hand (#164). This test compiles each examples//*.glsl as // crafter-build does (Vulkan rules, SPIR-V 1.4, shaders/ on the include // path), with the stage taken from the file name, and a shader that includes // ui-shared.glsl a second time for the descriptor-set build its example also // registers. A file whose name gives no stage fails rather than being // skipped, so a new shader is never silently left out. // // Needs glslang on the PATH (a required tool in project.cpp), which need not // be the glslang crafter-build links, and the source tree, which it finds // from the build output directory. No GPU. #include import std; namespace fs = std::filesystem; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } int RunCommand(const std::string& cmd) { int status = std::system(cmd.c_str()); if (status == -1) return -1; return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f); } std::string ReadFile(const fs::path& path) { std::ifstream in(path, std::ios::binary); return {std::istreambuf_iterator(in), std::istreambuf_iterator()}; } // glslang's -S name for the stage, from the file names the examples use: // raygen.glsl, closesthit.glsl, ..., .comp.glsl. Empty when unknown. std::string_view StageOf(const fs::path& shader) { const std::string stem = shader.stem().string(); if (stem.ends_with(".comp")) return "comp"; constexpr std::pair stages[] = { {"raygen", "rgen"}, {"intersection", "rint"}, {"anyhit", "rahit"}, {"closesthit", "rchit"}, {"miss", "rmiss"}, }; for (const auto& [name, stage] : stages) { if (stem == name) return stage; } return {}; } // The directory holding examples/ and shaders/, whether the test runs from // its build output directory or the project root, as UIFusedShader finds its // shader. fs::path LocateRoot() { for (const fs::path candidate : {".", "..", "../..", "../../.."}) { std::error_code ec; if (fs::is_directory(candidate / "examples", ec) && fs::is_regular_file(candidate / "shaders" / "ui-shared.glsl", ec)) { return candidate; } } return {}; } } // namespace int main() { const fs::path root = LocateRoot(); if (root.empty()) { std::println(std::cerr, "could not find examples/ and shaders/ from {}", fs::current_path().string()); return EXIT_FAILURE; } const fs::path out = fs::temp_directory_path() / "crafter-example-shaders-test"; std::error_code ec; fs::create_directories(out, ec); // Each example's own directory only: its bin/ and build/ hold outputs. std::vector shaders; for (const fs::directory_entry& example : fs::directory_iterator(root / "examples")) { if (!example.is_directory()) continue; for (const fs::directory_entry& file : fs::directory_iterator(example.path())) { if (file.is_regular_file() && file.path().extension() == ".glsl") shaders.push_back(file.path()); } } std::ranges::sort(shaders); Check(!shaders.empty(), std::format("found the examples' shaders ({})", shaders.size())); for (const fs::path& shader : shaders) { const std::string name = (shader.parent_path().filename() / shader.filename()).generic_string(); const std::string_view stage = StageOf(shader); if (stage.empty()) { Check(false, std::format("{}: no stage known for this file name; name it after its stage or teach StageOf", name)); continue; } // A shader on ui-shared.glsl's UI_HEAP_* macros is registered twice // by its example, the second build for drivers without the heap. const bool setsBuild = ReadFile(shader).contains("ui-shared.glsl"); for (const bool sets : {false, true}) { if (sets && !setsBuild) continue; const std::string base = std::format("{}-{}{}", shader.parent_path().filename().string(), shader.stem().string(), sets ? ".sets" : ""); const fs::path log = out / (base + ".log"); const std::string compile = std::format( "glslang -V --target-env spirv1.4 -S {} {}-I\"{}\" -o \"{}\" \"{}\" > \"{}\" 2>&1", stage, sets ? "-DCRAFTER_GRAPHICS_DESCRIPTOR_SETS " : "", (root / "shaders").string(), (out / (base + ".spv")).string(), shader.string(), log.string()); const bool compiled = RunCommand(compile) == 0; if (!compiled) std::print("{}", ReadFile(log)); Check(compiled, std::format("{} compiles{}", name, sets ? " (descriptor-set build)" : "")); } } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }