Crafter.Graphics/tests/ExampleShaders/main.cpp
Jorijn van der Graaf 6e94cc6209
fix(rt): split Sponza's combined image sampler into a heap texture and sampler
examples/Sponza no longer built natively (#164). Its closest-hit declared a
descriptor_heap sampler2D, which glslang has rejected since c8253623
("Reject descriptor_heap combined image samplers"), and main.cpp wrote it
through VkResourceDescriptorDataEXT::pCombinedImageSampler, which that union
no longer has: the descriptor heap has no combined image samplers. Both went
unnoticed because nothing builds the examples.

Sponza
- closesthit.glsl declares texture2D and sampler heap arrays and joins them
  where it samples, as ui-shared.glsl and HeapDynamicIndexHit do. The
  sampler slot is a second spec constant beside the image slot, so the
  shader assumes nothing about where the host put it, and a constant index
  keeps the example running on NVIDIA 610.43.02 (see its README). The
  comment no longer says the image slot is offset by bufferStartElement;
  only buffer slots are.
- main.cpp gives the heap a sampler slot, writes the albedo as a
  SAMPLED_IMAGE from a view create-info, and writes the sampler into the
  sampler heap from a VkSamplerCreateInfo with vkWriteSamplerDescriptorsEXT,
  as UIRenderer::RegisterSampler does. SamplerVulkan's VkSampler had no use
  left. The sampler keeps its linear filtering and repeat addressing and
  drops anisotropy, which a hit shader cannot use: it samples at LOD 0,
  without derivatives.

Docs
- README.md and examples/Sponza/README.md quote the new declarations, and
  the Sponza README says why the texture and the sampler are two
  descriptors. Its texture2DArray materials pattern declared a
  descriptor_heap sampler2DArray, which glslang rejects as well; it now
  joins a texture2DArray and a sampler.
- examples/SponzaBench/README.md drops its caveat that Sponza does not
  compile.

Tests
- ExampleShaders compiles every GLSL shader under examples/ with glslang as
  crafter-build does for the example (Vulkan rules, SPIR-V 1.4, shaders/ on
  the include path, and the descriptor-set build where the example
  registers one), so a glslang that starts rejecting one fails a test
  rather than an example build nobody runs. It fails on Sponza's old
  closest-hit and needs glslang only.

Closes #164.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:25:52 +01:00

125 lines
5.4 KiB
C++

//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/<Name>/*.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 <cstdlib>
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<char>(in), std::istreambuf_iterator<char>()};
}
// glslang's -S name for the stage, from the file names the examples use:
// raygen.glsl, closesthit.glsl, ..., <name>.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<std::string_view, std::string_view> 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<fs::path> 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;
}