// 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 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(&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:" 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(stream), std::istreambuf_iterator()); }; 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; }